Преглед на файлове

Merge pull request #2988 from IvanSavenko/configurable_extensions

Extension of configurable object functionality
Ivan Savenko преди 2 години
родител
ревизия
454ba44ac5
променени са 81 файла, в които са добавени 2074 реда и са изтрити 1434 реда
  1. 7 4
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  2. 1 3
      AI/Nullkiller/Engine/PriorityEvaluator.h
  3. 2 2
      AI/VCAI/ResourceManager.cpp
  4. 13 0
      Global.h
  5. 2 2
      client/Client.h
  6. 1 1
      client/NetPacksClient.cpp
  7. 15 2
      client/windows/InfoWindows.cpp
  8. 0 2
      cmake_modules/VCMI_lib.cmake
  9. 13 5
      config/gameConfig.json
  10. 97 0
      config/objects/cartographer.json
  11. 35 0
      config/objects/coverOfDarkness.json
  12. 0 176
      config/objects/generic.json
  13. 44 0
      config/objects/magicSpring.json
  14. 39 0
      config/objects/magicWell.json
  15. 0 17
      config/objects/moddables.json
  16. 79 0
      config/objects/observatory.json
  17. 11 0
      config/objects/rewardableBonusing.json
  18. 14 0
      config/objects/rewardableOncePerHero.json
  19. 0 78
      config/objects/rewardableOncePerWeek.json
  20. 3 3
      config/objects/rewardablePickable.json
  21. 94 0
      config/objects/scholar.json
  22. 209 0
      config/objects/shrine.json
  23. 65 0
      config/objects/witchHut.json
  24. 115 4
      docs/modders/Map_Objects/Rewardable.md
  25. 51 89
      lib/CArtHandler.cpp
  26. 5 11
      lib/CArtHandler.h
  27. 2 3
      lib/CGameInfoCallback.cpp
  28. 1 0
      lib/CGeneralTextHandler.cpp
  29. 1 1
      lib/CSkillHandler.h
  30. 9 29
      lib/IGameCallback.cpp
  31. 5 15
      lib/IGameCallback.h
  32. 0 3
      lib/JsonNode.cpp
  33. 306 158
      lib/JsonRandom.cpp
  34. 18 15
      lib/JsonRandom.h
  35. 3 6
      lib/NetPacks.h
  36. 5 7
      lib/NetPacksLib.cpp
  37. 6 11
      lib/TerrainHandler.cpp
  38. 3 2
      lib/TerrainHandler.h
  39. 10 0
      lib/constants/EntityIdentifiers.cpp
  40. 5 3
      lib/constants/EntityIdentifiers.h
  41. 6 0
      lib/constants/Enumerations.h
  42. 1 1
      lib/gameState/CGameState.cpp
  43. 1 1
      lib/int3.h
  44. 15 11
      lib/mapObjectConstructors/CBankInstanceConstructor.cpp
  45. 0 6
      lib/mapObjectConstructors/CObjectClassesHandler.cpp
  46. 1 2
      lib/mapObjectConstructors/CRewardableConstructor.h
  47. 3 1
      lib/mapObjectConstructors/CommonConstructors.cpp
  48. 2 1
      lib/mapObjectConstructors/DwellingInstanceConstructor.cpp
  49. 0 42
      lib/mapObjectConstructors/ShrineInstanceConstructor.cpp
  50. 0 34
      lib/mapObjectConstructors/ShrineInstanceConstructor.h
  51. 37 22
      lib/mapObjects/CBank.cpp
  52. 2 0
      lib/mapObjects/CBank.h
  53. 7 7
      lib/mapObjects/CGHeroInstance.cpp
  54. 1 1
      lib/mapObjects/CGHeroInstance.h
  55. 19 0
      lib/mapObjects/CGObjectInstance.cpp
  56. 6 0
      lib/mapObjects/CGObjectInstance.h
  57. 2 0
      lib/mapObjects/CGTownBuilding.cpp
  58. 1 5
      lib/mapObjects/CGTownInstance.cpp
  59. 121 20
      lib/mapObjects/CRewardableObject.cpp
  60. 16 7
      lib/mapObjects/CRewardableObject.h
  61. 2 398
      lib/mapObjects/MiscObjects.cpp
  62. 0 85
      lib/mapObjects/MiscObjects.h
  63. 101 17
      lib/mapping/MapFormatH3M.cpp
  64. 3 3
      lib/mapping/MapFormatH3M.h
  65. 1 1
      lib/pathfinder/CPathfinder.cpp
  66. 0 12
      lib/registerTypes/RegisterTypes.h
  67. 54 0
      lib/rewardable/Configuration.cpp
  68. 59 7
      lib/rewardable/Configuration.h
  69. 167 56
      lib/rewardable/Info.cpp
  70. 8 1
      lib/rewardable/Info.h
  71. 55 0
      lib/rewardable/Interface.cpp
  72. 0 2
      lib/rewardable/Interface.h
  73. 9 0
      lib/rewardable/Limiter.cpp
  74. 9 0
      lib/rewardable/Limiter.h
  75. 14 6
      lib/rewardable/Reward.cpp
  76. 34 4
      lib/rewardable/Reward.h
  77. 2 2
      scripting/lua/api/netpacks/SetResources.cpp
  78. 20 21
      server/CGameHandler.cpp
  79. 2 2
      server/CGameHandler.h
  80. 2 2
      server/processors/PlayerMessageProcessor.cpp
  81. 2 2
      test/mock/mock_IGameCallback.h

+ 7 - 4
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -528,13 +528,16 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 	}
 }
 
-float RewardEvaluator::evaluateWitchHutSkillScore(const CGWitchHut * hut, const CGHeroInstance * hero, HeroRole role) const
+float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
 {
+	auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
+	assert(rewardable);
+
+	auto skill = SecondarySkill(*rewardable->configuration.getVariable("secondarySkill", "gainedSkill"));
+
 	if(!hut->wasVisited(hero->tempOwner))
 		return role == HeroRole::SCOUT ? 2 : 0;
 
-	auto skill = SecondarySkill(hut->ability);
-
 	if(hero->getSecSkillLevel(skill) != SecSkillLevel::NONE
 		|| hero->secSkills.size() >= GameConstants::SKILL_PER_HERO)
 		return 0;
@@ -575,7 +578,7 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
 	case Obj::LIBRARY_OF_ENLIGHTENMENT:
 		return 8;
 	case Obj::WITCH_HUT:
-		return evaluateWitchHutSkillScore(dynamic_cast<const CGWitchHut *>(target), hero, role);
+		return evaluateWitchHutSkillScore(target, hero, role);
 	case Obj::PANDORAS_BOX:
 		//Can contains experience, spells, or skills (only on custom maps)
 		return 2.5f;

+ 1 - 3
AI/Nullkiller/Engine/PriorityEvaluator.h

@@ -18,8 +18,6 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-class CGWitchHut;
-
 VCMI_LIB_NAMESPACE_END
 
 namespace NKAI
@@ -43,7 +41,7 @@ public:
 	float getResourceRequirementStrength(int resType) const;
 	float getStrategicalValue(const CGObjectInstance * target) const;
 	float getTotalResourceRequirementStrength(int resType) const;
-	float evaluateWitchHutSkillScore(const CGWitchHut * hut, const CGHeroInstance * hero, HeroRole role) const;
+	float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
 	float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
 	int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const;
 	uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;

+ 2 - 2
AI/VCAI/ResourceManager.cpp

@@ -90,7 +90,7 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o
 {
 	auto allResources = cb->getResourceAmount();
 	auto income = estimateIncome();
-	GameResID resourceType = EGameResID::INVALID;
+	GameResID resourceType = EGameResID::NONE;
 	TResource amountToCollect = 0;
 
 	using resPair = std::pair<GameResID, TResource>;
@@ -129,7 +129,7 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o
 			break;
 		}
 	}
-	if (resourceType == EGameResID::INVALID) //no needed resources has 0 income,
+	if (resourceType == EGameResID::NONE) //no needed resources has 0 income,
 	{
 		//find the one which takes longest to collect
 		using timePair = std::pair<GameResID, float>;

+ 13 - 0
Global.h

@@ -475,6 +475,19 @@ namespace vstd
 		}
 	}
 
+	template<typename Elem, typename Predicate>
+	void erase_if(std::unordered_set<Elem> &setContainer, Predicate pred)
+	{
+		auto itr = setContainer.begin();
+		auto endItr = setContainer.end();
+		while(itr != endItr)
+		{
+			auto tmpItr = itr++;
+			if(pred(*tmpItr))
+				setContainer.erase(tmpItr);
+		}
+	}
+
 	//works for map and std::map, maybe something else
 	template<typename Key, typename Val, typename Predicate>
 	void erase_if(std::map<Key, Val> &container, Predicate pred)

+ 2 - 2
client/Client.h

@@ -209,8 +209,8 @@ public:
 	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {};
 	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {};
 
-	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {}
-	void changeFogOfWar(std::unordered_set<int3> & tiles, PlayerColor player, bool hide) override {}
+	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
+	void changeFogOfWar(std::unordered_set<int3> & tiles, PlayerColor player, ETileVisibility mode) override {}
 
 	void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {}
 

+ 1 - 1
client/NetPacksClient.cpp

@@ -177,7 +177,7 @@ void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack)
 		}
 		if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES)
 		{
-			if(pack.mode)
+			if(pack.mode == ETileVisibility::REVEALED)
 				i.second->tileRevealed(pack.tiles);
 			else
 				i.second->tileHidden(pack.tiles);

+ 15 - 2
client/windows/InfoWindows.cpp

@@ -351,10 +351,23 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p,
 	}
 	else
 	{
+		std::vector<Component> components;
+		if (settings["general"]["enableUiEnhancements"].Bool())
+		{
+			if(LOCPLINT->localState->getCurrentHero())
+				components = obj->getPopupComponents(LOCPLINT->localState->getCurrentHero());
+			else
+				components = obj->getPopupComponents(LOCPLINT->playerID);
+		}
+
+		std::vector<std::shared_ptr<CComponent>> guiComponents;
+		for (auto & component : components)
+			guiComponents.push_back(std::make_shared<CComponent>(component));
+
 		if(LOCPLINT->localState->getCurrentHero())
-			CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->localState->getCurrentHero()));
+			CRClickPopup::createAndPush(obj->getPopupText(LOCPLINT->localState->getCurrentHero()), guiComponents);
 		else
-			CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->playerID));
+			CRClickPopup::createAndPush(obj->getPopupText(LOCPLINT->playerID), guiComponents);
 	}
 }
 

+ 0 - 2
cmake_modules/VCMI_lib.cmake

@@ -81,7 +81,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/mapObjectConstructors/DwellingInstanceConstructor.cpp
 		${MAIN_LIB_DIR}/mapObjectConstructors/HillFortInstanceConstructor.cpp
 		${MAIN_LIB_DIR}/mapObjectConstructors/ShipyardInstanceConstructor.cpp
-		${MAIN_LIB_DIR}/mapObjectConstructors/ShrineInstanceConstructor.cpp
 
 		${MAIN_LIB_DIR}/mapObjects/CArmedInstance.cpp
 		${MAIN_LIB_DIR}/mapObjects/CBank.cpp
@@ -425,7 +424,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/mapObjectConstructors/IObjectInfo.h
 		${MAIN_LIB_DIR}/mapObjectConstructors/RandomMapInfo.h
 		${MAIN_LIB_DIR}/mapObjectConstructors/ShipyardInstanceConstructor.h
-		${MAIN_LIB_DIR}/mapObjectConstructors/ShrineInstanceConstructor.h
 		${MAIN_LIB_DIR}/mapObjectConstructors/SObjectSounds.h
 
 		${MAIN_LIB_DIR}/mapObjects/CArmedInstance.h

+ 13 - 5
config/gameConfig.json

@@ -48,15 +48,23 @@
 
 	"objects" :
 	[
-		"config/objects/generic.json",
-		"config/objects/moddables.json",
+		"config/objects/cartographer.json",
+		"config/objects/coverOfDarkness.json",
 		"config/objects/creatureBanks.json",
 		"config/objects/dwellings.json",
+		"config/objects/generic.json",
+		"config/objects/magicSpring.json",
+		"config/objects/magicWell.json",
+		"config/objects/moddables.json",
+		"config/objects/observatory.json",
+		"config/objects/rewardableBonusing.json",
+		"config/objects/rewardableOncePerHero.json",
 		"config/objects/rewardableOncePerWeek.json",
-		"config/objects/rewardablePickable.json",
 		"config/objects/rewardableOnceVisitable.json",
-		"config/objects/rewardableOncePerHero.json",
-		"config/objects/rewardableBonusing.json"
+		"config/objects/rewardablePickable.json",
+		"config/objects/scholar.json",
+		"config/objects/shrine.json",
+		"config/objects/witchHut.json"
 	],
 
 	"artifacts" :

+ 97 - 0
config/objects/cartographer.json

@@ -0,0 +1,97 @@
+{
+	"cartographer" : {
+		"index" :13,
+		"handler": "configurable",
+		"lastReservedIndex" : 2,
+		"base" : {
+			"sounds" : {
+				"visit" : ["LIGHTHOUSE"]
+			}
+		},
+		"types" : {
+			"cartographerWater" : {
+				"index" : 0,
+				"aiValue" : 5000,
+				"rmg" : {
+					"zoneLimit" : 1,
+					"value" : 5000,
+					"rarity" : 20
+				},
+				"compatibilityIdentifiers" : [ "water" ],
+				"visitMode" : "unlimited",
+				"canRefuse" : true,
+				"rewards" : [
+					{
+						"limiter" : { "resources" : { "gold" : 1000 } },
+						"message" : 25,
+						"resources" : {
+							"gold" : -1000
+						},
+						"revealTiles" : {
+							"water" : 1
+						}
+					}
+				],
+				"onEmptyMessage" : 28,
+				"onVisitedMessage" : 24
+			},
+			"cartographerLand" : {
+				"index" : 1,
+				"aiValue": 10000,
+				"rmg" : {
+					"zoneLimit" : 1,
+					"value" : 10000,
+					"rarity" : 2
+				},
+				"compatibilityIdentifiers" : [ "land" ],
+				"visitMode" : "unlimited",
+				"canRefuse" : true,
+				"rewards" : [
+					{
+						"limiter" : { "resources" : { "gold" : 1000 } },
+						"message" : 26,
+						"resources" : {
+							"gold" : -1000
+						},
+						"revealTiles" : {
+							"surface" : 1,
+							"water" : -1,
+							"rock" : -1
+						}
+					}
+				],
+				"onEmptyMessage" : 28,
+				"onVisitedMessage" : 24
+			},
+			"cartographerSubterranean" : {
+				"index" : 2,
+				"aiValue" : 7500,
+				"rmg" : {
+					"zoneLimit" : 1,
+					"value" : 7500,
+					"rarity" : 20
+				},
+				"compatibilityIdentifiers" : [ "subterra" ],
+				"visitMode" : "unlimited",
+				"canRefuse" : true,
+				"rewards" : [
+					{
+						"limiter" : { "resources" : { "gold" : 1000 } },
+						"message" : 27,
+						"resources" : {
+							"gold" : -1000
+						},
+						"revealTiles" : {
+							"subterra" : 1,
+							"water" : -1,
+							"rock" : -1,
+							"surface" : -1
+						}
+					}
+				],
+				"onEmptyMessage" : 28,
+				"onVisitedMessage" : 24
+			}
+		}
+	}
+}

+ 35 - 0
config/objects/coverOfDarkness.json

@@ -0,0 +1,35 @@
+{
+	"coverOfDarkness" : {
+		"index" :15,
+		"handler" : "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["LIGHTHOUSE"]
+			}
+		},
+		"types" : {
+			"coverOfDarkness" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+				},
+				
+				"compatibilityIdentifiers" : [ "object" ],
+				"visitMode" : "unlimited",
+				"rewards" : [
+					{
+						"message" : 31,
+						"revealTiles" : {
+							"radius" : 20,
+							"surface" : 1,
+							"subterra" : 1,
+							"water" : 1,
+							"rock" : 1,
+							"hide" : true
+						}
+					}
+				]
+			}
+		}
+	}
+}

+ 0 - 176
config/objects/generic.json

@@ -156,70 +156,6 @@
 		}
 	},
 
-	"redwoodObservatory" : {
-		"index" :58,
-		"handler" : "observatory",
-		"base" : {
-			"sounds" : {
-				"visit" : ["LIGHTHOUSE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 750,
-				"templates" :
-				{
-					"base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] },
-					"snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] }
-				},
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 750,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"pillarOfFire" : {
-		"index" :60,
-		"handler" : "observatory",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPFIRE"],
-				"visit" : ["LIGHTHOUSE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 750,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 750,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"coverOfDarkness" : {
-		"index" :15,
-		"handler" : "observatory",
-		"base" : {
-			"sounds" : {
-				"visit" : ["LIGHTHOUSE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-				}
-			}
-		}
-	},
-
 	"whirlpool" : {
 		"index" :111,
 		"handler" : "whirlpool",
@@ -294,78 +230,6 @@
 			}
 		}
 	},
-	"shrineOfMagicLevel1" : {//incantation
-		"index" :88,
-		"handler" : "shrine",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPSHRIN"],
-				"visit" : ["TEMPLE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 500,
-				"rmg" : {
-					"value"		: 500,
-					"rarity"	: 100
-				},
-				"visitText" : 127,
-				"spell" : {
-					"level" : 1
-				}
-			}
-		}
-	},
-	"shrineOfMagicLevel2" : {//gesture
-		"index" :89,
-		"handler" : "shrine",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPSHRIN"],
-				"visit" : ["TEMPLE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 2000,
-				"rmg" : {
-					"value"		: 2000,
-					"rarity"	: 100
-				},
-				"visitText" : 128,
-				"spell" : {
-					"level" : 2
-				}
-			}
-		}
-	},
-	"shrineOfMagicLevel3" : {//thinking
-		"index" :90,
-		"handler" : "shrine",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPSHRIN"],
-				"visit" : ["TEMPLE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 3000,
-				"rmg" : {
-					"value"		: 3000,
-					"rarity"	: 100
-				},
-				"visitText" : 129,
-				"spell" : {
-					"level" : 3
-				}
-			}
-		}
-	},
 	"eyeOfTheMagi" : {
 		"index" :27,
 		"handler" : "magi",
@@ -454,26 +318,6 @@
 			}
 		}
 	},
-	"scholar" : {
-		"index" :81,
-		"handler" : "scholar",
-		"base" : {
-			"sounds" : {
-				"visit" : ["GAZEBO"],
-				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"value"		: 1500,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
 	"shipyard" : {
 		"index" :87,
 		"handler" : "shipyard",
@@ -587,26 +431,6 @@
 			}
 		}
 	},
-	"witchHut" : {
-		"index" :113,
-		"handler" : "witch",
-		"base" : {
-			"sounds" : {
-				"visit" : ["GAZEBO"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"zoneLimit"	: 3,
-					"value"		: 1500,
-					"rarity"	: 80
-				}
-			}
-		}
-	},
 	"questGuard" : {
 		"index" :215,
 		"handler" : "questGuard",

+ 44 - 0
config/objects/magicSpring.json

@@ -0,0 +1,44 @@
+{
+	"magicSpring" : {
+		"index" : 48,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPFOUN"],
+				"visit" : ["FAERIE"]
+			}
+		},
+		"types" : {
+			"magicSpring" : {
+				"index" : 0,
+				"aiValue" : 500,
+				//banned due to problems with 2 viistable offsets
+				//"rmg" : {
+				//	"zoneLimit"	: 1,
+				//	"value"		: 500,
+				//	"rarity"	: 50
+				//},
+				"compatibilityIdentifiers" : [ "object" ],
+
+				"onEmptyMessage" : 76,
+				"onVisitedMessage" : 75,
+				"description" : "@core.xtrainfo.15",
+				"resetParameters" : {
+					"period" : 7,
+					"visitors" : true
+				},
+				"visitMode" : "once",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"limiter" : {
+							"noneOf" : [ { "manaPercentage" : 200 } ]
+						},
+						"message" : 74,
+						"manaPercentage" : 200
+					}
+				]
+			}
+		}
+	}
+}

+ 39 - 0
config/objects/magicWell.json

@@ -0,0 +1,39 @@
+{
+	"magicWell" : {
+		"index" :49,
+		"handler" : "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["FAERIE"]
+			}
+		},
+		"types" : {
+			"magicWell" : {
+				"index" : 0,
+				"aiValue" : 250,
+				"rmg" : {
+					"zoneLimit" : 1,
+					"value"		: 250,
+					"rarity"	: 100
+				},
+				"compatibilityIdentifiers" : [ "object" ],
+
+				"onEmptyMessage" : 79,
+				"onVisitedMessage" : 78,
+				"description" : "@core.xtrainfo.25",
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"limiter" : {
+							"noneOf" : [ { "manaPercentage" : 100 } ]
+						},
+						"bonuses" : [ { "type" : "NONE", "duration" : "ONE_DAY"} ],
+						"message" : 77,
+						"manaPercentage" : 100
+					}
+				]	
+			},
+		}
+	}
+}

+ 0 - 17
config/objects/moddables.json

@@ -255,23 +255,6 @@
 		}
 	},
 
-	// subtype: different revealed areas
-	"cartographer" : {
-		"index" :13,
-		"handler": "cartographer",
-		"lastReservedIndex" : 2,
-		"base" : {
-			"sounds" : {
-				"visit" : ["LIGHTHOUSE"]
-			}
-		},
-		"types" : {
-			"water" : { "index" : 0, "aiValue" : 5000, "rmg" : { "zoneLimit" : 1,  "value" : 5000, "rarity" : 20 } },
-			"land" : { "index" : 1, "aiValue": 10000, "rmg" : { "zoneLimit" : 1,  "value" : 10000, "rarity" : 20 } },
-			"subterra" : { "index" : 2, "aiValue" : 7500, "rmg" : { "zoneLimit" : 1,  "value" : 7500, "rarity" : 20 } }
-		}
-	},
-
 	// subtype: resource ID
 	"mine" : {
 		"index" :53,

+ 79 - 0
config/objects/observatory.json

@@ -0,0 +1,79 @@
+{
+	"redwoodObservatory" : {
+		"index" :58,
+		"handler" : "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["LIGHTHOUSE"]
+			}
+		},
+		"types" : {
+			"redwoodObservatory" : {
+				"index" : 0,
+				"aiValue" : 750,
+				"templates" :
+				{
+					"base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] },
+					"snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] }
+				},
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 750,
+					"rarity"	: 100
+				},
+				
+				"compatibilityIdentifiers" : [ "object" ],
+				"visitMode" : "unlimited",
+				"rewards" : [
+					{
+						"message" : 98,
+						"revealTiles" : {
+							"radius" : 20,
+							"surface" : 1,
+							"subterra" : 1,
+							"water" : 1,
+							"rock" : 1
+						}
+					}
+				]
+			}
+		}
+	},
+
+	"pillarOfFire" : {
+		"index" :60,
+		"handler" : "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPFIRE"],
+				"visit" : ["LIGHTHOUSE"]
+			}
+		},
+		"types" : {
+			"pillarOfFire" : {
+				"index" : 0,
+				"aiValue" : 750,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 750,
+					"rarity"	: 100
+				},
+				
+				"compatibilityIdentifiers" : [ "object" ],
+				"visitMode" : "unlimited",
+				"rewards" : [
+					{
+						"message" : 99,
+						"revealTiles" : {
+							"radius" : 20,
+							"surface" : 1,
+							"subterra" : 1,
+							"water" : 1,
+							"rock" : 1
+						}
+					}
+				]
+			}
+		}
+	}
+}

+ 11 - 0
config/objects/rewardableBonusing.json

@@ -23,6 +23,7 @@
 
 				"blockedVisitable" : true,
 				"onVisitedMessage" : 22,
+				"description" : "@core.xtrainfo.0",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -54,6 +55,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 30,
+				"description" : "@core.xtrainfo.1",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -87,6 +89,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 50,
+				"description" : "@core.xtrainfo.2",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -119,6 +122,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 56,
+				"description" : "@core.xtrainfo.3",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"resetParameters" : {
@@ -172,6 +176,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 58,
+				"description" : "@core.xtrainfo.0",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -204,6 +209,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 63,
+				"description" : "@core.xtrainfo.22",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -261,6 +267,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 82,
+				"description" : "@core.xtrainfo.2",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -292,6 +299,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 95,
+				"description" : "@core.xtrainfo.13",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -383,6 +391,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 141,
+				"description" : "@core.xtrainfo.23",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -420,6 +429,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 111,
+				"description" : "@core.xtrainfo.17",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -455,6 +465,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 167,
+				"description" : "@core.xtrainfo.13",
 				"visitMode" : "bonus",
 				"selectMode" : "selectFirst",
 				"rewards" : [

+ 14 - 0
config/objects/rewardableOncePerHero.json

@@ -55,6 +55,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 40,
+				"description" : "@core.xtrainfo.7",
 				"visitMode" : "hero",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -86,6 +87,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 60,
+				"description" : "@core.xtrainfo.4",
 				"visitMode" : "hero",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -117,6 +119,7 @@
 
 				"onVisitedMessage" : 67,
 				"onEmptyMessage" : 68,
+				"description" : "@core.xtrainfo.6",
 				"visitMode" : "hero",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -161,6 +164,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 81,
+				"description" : "@core.xtrainfo.8",
 				"visitMode" : "hero",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -192,6 +196,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 101,
+				"description" : "@core.xtrainfo.11",
 				"visitMode" : "hero",
 				"selectMode" : "selectFirst",
 				"rewards" : [
@@ -233,16 +238,21 @@
 					}
 				],
 				"onVisitedMessage" : 147,
+				"description" : "@core.xtrainfo.18",
 				"visitMode" : "hero",
 				"selectMode" : "selectFirst",
 				"canRefuse" : true,
+				"showScoutedPreview" : true,
+
 				"rewards" : [
 					{
+						"description" : "@core.arraytxt.202",
 						"message" : 148,
 						"appearChance" : { "max" : 34 },
 						"gainedLevels" : 1
 					},
 					{
+						"description" : "@core.arraytxt.203",
 						"message" : 149,
 						"appearChance" : { "min" : 34, "max" : 67 },
 						"limiter" : { "resources" : { "gold" : 2000 } },
@@ -250,6 +260,7 @@
 						"gainedLevels" : 1
 					},
 					{
+						"description" : "@core.arraytxt.204",
 						"message" : 151,
 						"appearChance" : { "min" : 67 },
 						"limiter" : { "resources" : { "gems" : 10 } },
@@ -282,6 +293,7 @@
 				"onSelectMessage" : 71,
 				"onVisitedMessage" : 72,
 				"onEmptyMessage" : 73,
+				"description" : "@core.xtrainfo.9",
 				"visitMode" : "hero",
 				"selectMode" : "selectPlayer",
 				"canRefuse" : true,
@@ -322,6 +334,7 @@
 				"onSelectMessage" : 158,
 				"onVisitedMessage" : 159,
 				"onEmptyMessage" : 160,
+				"description" : "@core.xtrainfo.10",
 				"visitMode" : "hero",
 				"selectMode" : "selectPlayer",
 				"canRefuse" : true,
@@ -360,6 +373,7 @@
 				"compatibilityIdentifiers" : [ "object" ],
 
 				"onVisitedMessage" : 144,
+				"description" : "@core.xtrainfo.5",
 				"visitMode" : "hero",
 				"selectMode" : "selectFirst",
 				"rewards" : [

+ 0 - 78
config/objects/rewardableOncePerWeek.json

@@ -1,82 +1,4 @@
 {
-	/// These are objects that covered by concept of "configurable object" and have their entire configuration in this config
-	"magicWell" : {
-		"index" :49,
-		"handler" : "configurable",
-		"base" : {
-			"sounds" : {
-				"visit" : ["FAERIE"]
-			}
-		},
-		"types" : {
-			"magicWell" : {
-				"index" : 0,
-				"aiValue" : 250,
-				"rmg" : {
-					"zoneLimit" : 1,
-					"value"		: 250,
-					"rarity"	: 100
-				},
-				"compatibilityIdentifiers" : [ "object" ],
-
-				"onEmptyMessage" : 79,
-				"onVisitedMessage" : 78,
-				"visitMode" : "bonus",
-				"selectMode" : "selectFirst",
-				"rewards" : [
-					{
-						"limiter" : {
-							"noneOf" : [ { "manaPercentage" : 100 } ]
-						},
-						"bonuses" : [ { "type" : "NONE", "duration" : "ONE_DAY"} ],
-						"message" : 77,
-						"manaPercentage" : 100
-					}
-				]	
-			},
-		}
-	},
-	"magicSpring" : {
-		"index" : 48,
-		"handler": "configurable",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPFOUN"],
-				"visit" : ["FAERIE"]
-			}
-		},
-		"types" : {
-			"magicSpring" : {
-				"index" : 0,
-				"aiValue" : 500,
-				//banned due to problems with 2 viistable offsets
-				//"rmg" : {
-				//	"zoneLimit"	: 1,
-				//	"value"		: 500,
-				//	"rarity"	: 50
-				//},
-				"compatibilityIdentifiers" : [ "object" ],
-
-				"onEmptyMessage" : 76,
-				"onVisitedMessage" : 75,
-				"resetParameters" : {
-					"period" : 7,
-					"visitors" : true
-				},
-				"visitMode" : "once",
-				"selectMode" : "selectFirst",
-				"rewards" : [
-					{
-						"limiter" : {
-							"noneOf" : [ { "manaPercentage" : 200 } ]
-						},
-						"message" : 74,
-						"manaPercentage" : 200
-					}
-				]				
-			}
-		}
-	},
 	"mysticalGarden" : {
 		"index" : 55,
 		"handler": "configurable",

+ 3 - 3
config/objects/rewardablePickable.json

@@ -235,7 +235,7 @@
 					},
 					{
 						"appearChance" : { "max" : 33 },
-						"gainedExp" : 1500,
+						"heroExperience" : 1500,
 						"removeObject" : true,
 					},
 					{
@@ -245,7 +245,7 @@
 					},
 					{
 						"appearChance" : { "min" : 33, "max" : 65 },
-						"gainedExp" : 1000,
+						"heroExperience" : 1000,
 						"removeObject" : true,
 					},
 					{
@@ -255,7 +255,7 @@
 					},
 					{
 						"appearChance" : { "min" : 65, "max" : 95 },
-						"gainedExp" : 500,
+						"heroExperience" : 500,
 						"removeObject" : true,
 					},
 					{

+ 94 - 0
config/objects/scholar.json

@@ -0,0 +1,94 @@
+{
+	"scholar" : {
+		"index" :81,
+		"handler" : "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["GAZEBO"],
+				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
+			}
+		},
+		"types" : {
+			"scholar" : {
+				"index" : 0,
+				"aiValue" : 1500,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 100
+				},
+				"compatibilityIdentifiers" : [ "object" ],
+				
+				"visitMode" : "unlimited",
+				"blockedVisitable" : true,
+				
+				"variables" : {
+					"spell" : {
+						"gainedSpell" : { // Note: this variable name is used by engine for H3M loading
+						}
+					},
+					"secondarySkill" : {
+						"gainedSkill" : { // Note: this variable name is used by engine for H3M loading
+						}
+					},
+					"primarySkill" : {
+						"gainedStat" : { // Note: this variable name is used by engine for H3M loading
+						}
+					}
+				},
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"appearChance" : { "min" : 0, "max" : 33 },
+						"message" : 115,
+						"limiter" : {
+							"canLearnSpells" : [
+								"@gainedSpell"
+							]
+						},
+						"spells" : [
+							"@gainedSpell"
+						],
+						"removeObject" : true
+					},
+					{
+						"appearChance" : { "min" : 33, "max" : 66 },
+						"message" : 115,
+						"limiter" : {
+							// Hero does not have this skill at expert
+							"noneOf" : [
+									{
+									"secondary" : {
+										"@gainedSkill" : 3
+									}
+								}
+							],
+							// And have either free skill slot or this skill
+							"anyOf" : [
+								{
+									"canLearnSkills" : true
+								},
+								{
+									"secondary" : {
+										"@gainedSkill" : 1
+									}
+								}
+							]
+						},
+						"secondary" : {
+							"@gainedSkill" : 1
+						},
+						"removeObject" : true
+					},
+					{
+						// Always present - fallback if hero can't learn secondary / spell
+						"message" : 115,
+						"primary" : {
+							"@gainedStat" : 1
+						},
+						"removeObject" : true
+					}
+				]
+			}
+		}
+	}
+}

+ 209 - 0
config/objects/shrine.json

@@ -0,0 +1,209 @@
+{
+	"shrineOfMagicLevel1" : {//incantation
+		"index" :88,
+		"handler" : "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPSHRIN"],
+				"visit" : ["TEMPLE"]
+			}
+		},
+		"types" : {
+			"shrineOfMagicLevel1" : {
+				"index" : 0,
+				"aiValue" : 500,
+				"rmg" : {
+					"value"		: 500,
+					"rarity"	: 100
+				},
+				"compatibilityIdentifiers" : [ "object" ],
+				
+				"visitMode" : "limiter",
+				"visitedTooltip" : 354,
+				"description" : "@core.xtrainfo.19",
+				"showScoutedPreview" : true,
+
+				"variables" : {
+					"spell" : {
+						"gainedSpell" : { // Note: this variable name is used by engine for H3M loading
+							"level": 1
+						}
+					}
+				},
+				"visitLimiter" : {
+					"spells" : [
+						"@gainedSpell"
+					]
+				},
+				"rewards" : [
+					{
+						"limiter" : {
+							"canLearnSpells" : [
+								"@gainedSpell"
+							]
+						},
+						"spells" : [
+							"@gainedSpell"
+						],
+						"description" : "@core.genrltxt.355",
+						"message" : [ 127, "%s." ] // You learn new spell
+					}
+				],
+				"onVisitedMessage" : [ 127, "%s.", 174 ], // You already known this spell
+				"onEmpty" : [
+					{
+						"limiter" : {
+							"artifacts" : [
+								{
+									"type" : "spellBook"
+								}
+							]
+						},
+						"message" : [ 127, "%s.", 130 ] // No Wisdom
+					},
+					{
+						"message" : [ 127, "%s.", 131 ] // No spellbook
+					}
+				]
+			}
+		}
+	},
+	"shrineOfMagicLevel2" : {//gesture
+		"index" :89,
+		"handler" : "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPSHRIN"],
+				"visit" : ["TEMPLE"]
+			}
+		},
+		"types" : {
+			"shrineOfMagicLevel2" : {
+				"index" : 0,
+				"aiValue" : 2000,
+				"rmg" : {
+					"value"		: 2000,
+					"rarity"	: 100
+				},
+				"compatibilityIdentifiers" : [ "object" ],
+				
+				"visitMode" : "limiter",
+				"visitedTooltip" : 354,
+				"description" : "@core.xtrainfo.20",
+				"showScoutedPreview" : true,
+
+				"variables" : {
+					"spell" : {
+						"gainedSpell" : { // Note: this variable name is used by engine for H3M loading
+							"level": 2
+						}
+					}
+				},
+				"visitLimiter" : {
+					"spells" : [
+						"@gainedSpell"
+					]
+				},
+				"rewards" : [
+					{
+						"limiter" : {
+							"canLearnSpells" : [
+								"@gainedSpell"
+							]
+						},
+						"spells" : [
+							"@gainedSpell"
+						],
+						"description" : "@core.genrltxt.355",
+						"message" : [ 128, "%s." ] // You learn new spell
+					}
+				],
+				"onVisitedMessage" : [ 128, "%s.", 174 ], // You already known this spell
+				"onEmpty" : [
+					{
+						"limiter" : {
+							"artifacts" : [
+								{
+									"type" : "spellBook"
+								}
+							]
+						},
+						"message" : [ 128, "%s.", 130 ] // No Wisdom
+					},
+					{
+						"message" : [ 128, "%s.", 131 ] // No spellbook
+					}
+				]
+			}
+		}
+	},
+	"shrineOfMagicLevel3" : {//thinking
+		"index" :90,
+		"handler" : "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPSHRIN"],
+				"visit" : ["TEMPLE"]
+			}
+		},
+		"types" : {
+			"shrineOfMagicLevel3" : {
+				"index" : 0,
+				"aiValue" : 3000,
+				"rmg" : {
+					"value"		: 3000,
+					"rarity"	: 100
+				},
+				"compatibilityIdentifiers" : [ "object" ],
+				
+				"visitMode" : "limiter",
+				"visitedTooltip" : 354,
+				"description" : "@core.xtrainfo.21",
+				"showScoutedPreview" : true,
+
+				"variables" : {
+					"spell" : {
+						"gainedSpell" : { // Note: this variable name is used by engine for H3M loading
+							"level": 3
+						}
+					}
+				},
+				"visitLimiter" : {
+					"spells" : [
+						"@gainedSpell"
+					]
+				},
+				"rewards" : [
+					{
+						"limiter" : {
+							"canLearnSpells" : [
+								"@gainedSpell"
+							]
+						},
+						"spells" : [
+							"@gainedSpell"
+						],
+						"description" : "@core.genrltxt.355",
+						"message" : [ 129, "%s." ] // You learn new spell
+					}
+				],
+				"onVisitedMessage" : [ 129, "%s.", 174 ], // You already known this spell
+				"onEmpty" : [
+					{
+						"limiter" : {
+							"artifacts" : [
+								{
+									"type" : "spellBook"
+								}
+							]
+						},
+						"message" : [ 129, "%s.", 130 ] // No Wisdom
+					},
+					{
+						"message" : [ 129, "%s.", 131 ] // No spellbook
+					}
+				]
+			}
+		}
+	}
+}

+ 65 - 0
config/objects/witchHut.json

@@ -0,0 +1,65 @@
+{
+	"witchHut" : {
+		"index" :113,
+		"handler" : "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["GAZEBO"]
+			}
+		},
+		"types" : {
+			"witchHut" : {
+				"index" : 0,
+				"aiValue" : 1500,
+				"rmg" : {
+					"zoneLimit"	: 3,
+					"value"		: 1500,
+					"rarity"	: 80
+				},
+				"compatibilityIdentifiers" : [ "object" ],
+				
+				"visitMode" : "limiter",
+				"visitedTooltip" : 354,
+				"description" : "@core.xtrainfo.12",
+				"showScoutedPreview" : true,
+
+				"variables" : {
+					"secondarySkill" : {
+						"gainedSkill" : { // Note: this variable name is used by engine for H3M loading and by AI
+							"noneOf" : [
+								"leadership",
+								"necromancy"
+							]
+						}
+					}
+				},
+				"visitLimiter" : {
+					"secondary" : {
+						"@gainedSkill" : 1
+					}
+				},
+				"rewards" : [
+					{
+						"limiter" : {
+							"canLearnSkills" : true,
+							"noneOf" : [
+								{
+									"secondary" : {
+										"@gainedSkill" : 1
+									}
+								}
+							]
+						},
+						"description" : 355,
+						"secondary" : {
+							"@gainedSkill" : 1
+						},
+						"message" : 171 // Witch teaches you skill
+					}
+				],
+				"onVisitedMessage" : 172, // You already known this skill
+				"onEmptyMessage" : 173 // You know too much (no free slots)
+			}
+		}
+	}
+}

+ 115 - 4
docs/modders/Map_Objects/Rewardable.md

@@ -4,6 +4,7 @@
 - [Base object definition](#base-object-definition)
 - [Configurable object definition](#configurable-object-definition)
 - [Base object definition](#base-object-definition)
+- [Variables Parameters definition](#variables-parameters-definition)
 - [Reset Parameters definition](#reset-parameters-definition)
 - [Appear Chance definition](#appear-chance-definition)
 - [Configurable Properties](#configurable-properties)
@@ -17,12 +18,18 @@
 - - [Movement Percentage](#movement-percentage)
 - - [Primary Skills](#primary-skills)
 - - [Secondary Skills](#secondary-skills)
+- - [Can learn skills](#can-learn-skills)
 - - [Bonus System](#bonus-system)
 - - [Artifacts](#artifacts)
 - - [Spells](#spells)
+- - [Can learn spells](#can-learn-spells)
 - - [Creatures](#creatures)
 - - [Creatures Change](#creatures-change)
 - - [Spell cast](#spell-cast)
+- - [Fog of War](#fog-of-war)
+- - [Player color](#player-color)
+- - [Hero types](#hero-types)
+- - [Hero classes](#hero-classes)
 
 ## Base object definition
 Rewardable object is defined similarly to other objects, with key difference being `handler`. This field must be set to `"handler" : "configurable"` in order for vcmi to use this mode.
@@ -92,20 +99,47 @@ Rewardable object is defined similarly to other objects, with key difference bei
     // message that will be shown if this is the only available award
     "message": "{Warehouse of Crystal}"
 
+    // Alternative object description that will be used in place of generic description after player visits this object and reveals its content
+    // For example, Tree of Knowledge will display cost of levelup (gems or gold) only after object has been visited once
+    "description" : "",
+
     // object will be disappeared after taking reward is set to true
-    "removeObject": false
+    "removeObject": false,
 
     // See "Configurable Properties" section for additional parameters
     <additional properties>
   }
 ],
 
+/// List of variables shared between all rewards and limiters
+/// See "Variables" section for description
+"variables" : {
+}
+
 // If true, hero can not move to visitable tile of the object and will access this object from adjacent tile (e.g. Treasure Chest)
 "blockedVisitable" : true,
 
 // Message that will be shown if there are no applicable awards
 "onEmptyMessage": "",
 
+// Object description that will be shown when player right-clicks object
+"description" : "",
+
+// If set to true, right-clicking previously visited object would show preview of its content. For example, Witch Hut will show icon with provided skill
+"showScoutedPreview" : true,
+
+// Text that should be used if hero has not visited this object. If not specified, game will use standard "(Not visited)" text
+"notVisitedTooltip" : "",
+
+// Text that should be used if hero has already visited this object. If not specified, game will use standard "(Already visited)" text
+"visitedTooltip" : "",
+
+// Used only if visitMode is set to "limiter"
+// Hero that passes this limiter will be considered to have visited this object
+// Note that if player or his allies have never visited this object, it will always show up as "not visited"
+"visitLimiter" : {
+},
+
 // Alternatively, rewards for empty state:
 // Format is identical to "rewards" section, allowing to fine-tune behavior in this case, including giving awards or different messages to explain why object is "empty". For example, Tree of Knowledge will give different messages depending on whether it asks for gold or crystals
 "onEmpty" : [
@@ -133,6 +167,7 @@ Rewardable object is defined similarly to other objects, with key difference bei
 // determines who can revisit object before reset
 // "once",     - object can only be visited once. First visitor takes it all.
 // "hero",     - object can be visited if this hero has not visited it before
+// "limiter",  - object can be visited if hero fails to fulfill provided limiter
 // "player",   - object can be visited if this player has not visited it before
 // "bonus"     - object can be visited if hero no longer has bonus from this object (including any other object of the same type)
 // "unlimited" - no restriction on revisiting.
@@ -147,6 +182,39 @@ Rewardable object is defined similarly to other objects, with key difference bei
 }
 ```
 
+## Variables Parameters definition
+
+This property allows defining "variables" that are shared between all rewards and limiters of this object.
+Variables are randomized only once, so you can use them multiple times for example, to give skill only if hero does not have this skill (e.g. Witch Hut).
+
+Example of creation of a variable named "gainedSkill" of type "secondarySkill":
+```json
+"variables" : {
+	"secondarySkill" : {
+		"gainedSkill" : {
+			"noneOf" : [
+				"leadership",
+				"necromancy"
+			]
+		}
+	}
+}
+```
+
+Possible variable types:
+- number: can be used in any place that expects a number
+- artifact
+- spell
+- primarySkill
+- secondarySkill
+
+To reference variable in limiter prepend variable name with '@' symbol:
+```json
+"secondary" : {
+    "@gainedSkill" : 1
+},
+```
+
 ## Reset Parameters definition
 This property describes how object state should be reset. Objects without this field will never reset its state.
 - Period describes interval between object resets in day. Periods are counted from game start and not from hero visit, so reset duration of 7 will always reset object on new week & duration of 28 will always reset on new month.
@@ -220,7 +288,7 @@ Keep in mind, that all randomization is performed on map load and on object rese
 ```jsonc
 "resources": [
     {
-        "list" : [ "wood", "ore" ],
+        "anyOf" : [ "wood", "ore" ],
         "amount" : 10
     },
     {
@@ -349,13 +417,21 @@ Keep in mind, that all randomization is performed on map load and on object rese
 ]
 ```
 
+### Can learn skills
+
+- Can be used as limiter. Hero must have free skill slot to pass limiter
+
+```json
+    "canLearnSkills" : true
+```
+
 ### Bonus System
 - Can be used as reward, to grant bonus to player
 - if present, MORALE and LUCK bonus will add corresponding image component to UI.
 - Note that unlike most values, parameter of bonuses can NOT be randomized
 - Description can be string or number of corresponding string from `arraytxt.txt`
 
-```jsonc
+```json
 "bonuses" : [
     {
         "type" : "MORALE", 
@@ -414,6 +490,22 @@ Keep in mind, that all randomization is performed on map load and on object rese
 ],
 ```
 
+### Can learn spells
+
+- Can be used as limiter. Hero must be able to learn spell to pass the limiter
+- Hero is considered to not able to learn the spell if:
+- - he already has specified spell
+- - he does not have a spellbook 
+- - he does not have sufficient Wisdom level for this spell
+
+```json
+    "canLearnSpells" : [
+        "magicArrow"
+],
+```
+
+canLearnSpells
+
 ### Creatures
 - Can be used as limiter
 - Can be used as reward, to give new creatures to a hero
@@ -445,13 +537,32 @@ Keep in mind, that all randomization is performed on map load and on object rese
 - As reward, instantly casts adventure map spell for visiting hero. All checks for spell book, wisdom or presence of mana will be ignored. It's possible to specify school level at which spell will be casted. If it's necessary to reduce player's mana or do some checks, they shall be introduced as limiters and other rewards
 - School level possible values: 1 (basic), 2 (advanced), 3 (expert)
 
-```jsonc
+```json
 "spellCast" : {
     "spell" : "townPortal",
     "schoolLevel": 3
 }
 ```
 
+### Fog of War
+
+- Can NOT be used as limiter
+- Can be used as reward, to reveal or hide affected tiles
+- If radius is not specified, then all matching tiles on the map will be affected
+- It is possible to specify which terrain classes should be affected. Tile will be affected if sum of values its classes is positive. For example, `"water" : 1` will affect all water tiles, while `"surface" : 1, "subterra" : -1` will include terrains that have "surface" flag but do not have "subterra" flag
+- If 'hide' is set to true, then instead of revealing terrain, game will hide affected tiles for all other players
+
+```json
+"revealTiles" : {
+	"radius" : 20,
+	"surface" : 1,
+	"subterra" : 1,
+	"water" : 1,
+	"rock" : 1,
+	"hide" : true
+}
+```
+
 ### Player color
 - Can be used as limiter
 - Can NOT be used as reward

+ 51 - 89
lib/CArtHandler.cpp

@@ -609,49 +609,60 @@ void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node)
 
 ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function<bool(ArtifactID)> accepts)
 {
-	auto getAllowedArts = [&](std::vector<ConstTransitivePtr<CArtifact> > &out, std::vector<CArtifact*> *arts, CArtifact::EartClass flag)
+	std::set<ArtifactID> potentialPicks;
+
+	// Select artifacts that satisfy provided criterias
+	for (auto const * artifact : allowedArtifacts)
 	{
-		if (arts->empty()) //restock available arts
-			fillList(*arts, flag);
+		assert(artifact->aClass != CArtifact::ART_SPECIAL); // should be filtered out when allowedArtifacts is initialized
 
-		for (auto & arts_i : *arts)
-		{
-			if (accepts(arts_i->id))
-			{
-				CArtifact *art = arts_i;
-				out.emplace_back(art);
-			}
-		}
-	};
+		if ((flags & CArtifact::ART_TREASURE) == 0 && artifact->aClass == CArtifact::ART_TREASURE)
+			continue;
+
+		if ((flags & CArtifact::ART_MINOR) == 0 && artifact->aClass == CArtifact::ART_MINOR)
+			continue;
+
+		if ((flags & CArtifact::ART_MAJOR) == 0 && artifact->aClass == CArtifact::ART_MAJOR)
+			continue;
+
+		if ((flags & CArtifact::ART_RELIC) == 0 && artifact->aClass == CArtifact::ART_RELIC)
+			continue;
+
+		if (!accepts(artifact->id))
+			continue;
 
-	auto getAllowed = [&](std::vector<ConstTransitivePtr<CArtifact> > &out)
+		potentialPicks.insert(artifact->id);
+	}
+
+	return pickRandomArtifact(rand, potentialPicks);
+}
+
+ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, std::set<ArtifactID> potentialPicks)
+{
+	// No allowed artifacts at all - give Grail - this can't be banned (hopefully)
+	// FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior
+	if (potentialPicks.empty())
 	{
-		if (flags & CArtifact::ART_TREASURE)
-			getAllowedArts (out, &treasures, CArtifact::ART_TREASURE);
-		if (flags & CArtifact::ART_MINOR)
-			getAllowedArts (out, &minors, CArtifact::ART_MINOR);
-		if (flags & CArtifact::ART_MAJOR)
-			getAllowedArts (out, &majors, CArtifact::ART_MAJOR);
-		if (flags & CArtifact::ART_RELIC)
-			getAllowedArts (out, &relics, CArtifact::ART_RELIC);
-		if(out.empty()) //no artifact of specified rarity, we need to take another one
-		{
-			getAllowedArts (out, &treasures, CArtifact::ART_TREASURE);
-			getAllowedArts (out, &minors, CArtifact::ART_MINOR);
-			getAllowedArts (out, &majors, CArtifact::ART_MAJOR);
-			getAllowedArts (out, &relics, CArtifact::ART_RELIC);
-		}
-		if(out.empty()) //no arts are available at all
-		{
-			out.resize (64);
-			std::fill_n (out.begin(), 64, objects[2]); //Give Grail - this can't be banned (hopefully)
-		}
-	};
+		logGlobal->warn("Failed to find artifact that matches requested parameters!");
+		return ArtifactID::GRAIL;
+	}
+
+	// Find how many times least used artifacts were picked by randomizer
+	int leastUsedTimes = std::numeric_limits<int>::max();
+	for (auto const & artifact : potentialPicks)
+		if (allocatedArtifacts[artifact] < leastUsedTimes)
+			leastUsedTimes = allocatedArtifacts[artifact];
+
+	// Pick all artifacts that were used least number of times
+	std::set<ArtifactID> preferredPicks;
+	for (auto const & artifact : potentialPicks)
+		if (allocatedArtifacts[artifact] == leastUsedTimes)
+			preferredPicks.insert(artifact);
 
-	std::vector<ConstTransitivePtr<CArtifact> > out;
-	getAllowed(out);
-	ArtifactID artID = (*RandomGeneratorUtil::nextItem(out, rand))->id;
-	erasePickedArt(artID);
+	assert(!preferredPicks.empty());
+
+	ArtifactID artID = *RandomGeneratorUtil::nextItem(preferredPicks, rand);
+	allocatedArtifacts[artID] += 1; // record +1 more usage
 	return artID;
 }
 
@@ -712,16 +723,13 @@ bool CArtHandler::legalArtifact(const ArtifactID & id)
 void CArtHandler::initAllowedArtifactsList(const std::vector<bool> &allowed)
 {
 	allowedArtifacts.clear();
-	treasures.clear();
-	minors.clear();
-	majors.clear();
-	relics.clear();
+	allocatedArtifacts.clear();
 
 	for (ArtifactID i=ArtifactID::SPELLBOOK; i < ArtifactID(static_cast<si32>(objects.size())); i.advance(1))
 	{
 		if (allowed[i] && legalArtifact(ArtifactID(i)))
 			allowedArtifacts.push_back(objects[i]);
-			 //keep im mind that artifact can be worn by more than one type of bearer
+			//keep im mind that artifact can be worn by more than one type of bearer
 	}
 }
 
@@ -734,52 +742,6 @@ std::vector<bool> CArtHandler::getDefaultAllowed() const
 	return allowedArtifacts;
 }
 
-void CArtHandler::erasePickedArt(const ArtifactID & id)
-{
-	CArtifact *art = objects[id];
-
-	std::vector<CArtifact*> * artifactList = nullptr;
-	switch(art->aClass)
-	{
-		case CArtifact::ART_TREASURE:
-			artifactList = &treasures;
-			break;
-		case CArtifact::ART_MINOR:
-			artifactList = &minors;
-			break;
-		case CArtifact::ART_MAJOR:
-			artifactList = &majors;
-			break;
-		case CArtifact::ART_RELIC:
-			artifactList = &relics;
-			break;
-		default:
-			logMod->warn("Problem: cannot find list for artifact %s, strange class. (special?)", art->getNameTranslated());
-			return;
-	}
-
-	if(artifactList->empty())
-		fillList(*artifactList, art->aClass);
-
-	auto itr = vstd::find(*artifactList, art);
-	if(itr != artifactList->end())
-	{
-		artifactList->erase(itr);
-	}
-	else
-		logMod->warn("Problem: cannot erase artifact %s from list, it was not present", art->getNameTranslated());
-}
-
-void CArtHandler::fillList( std::vector<CArtifact*> &listToBeFilled, CArtifact::EartClass artifactClass )
-{
-	assert(listToBeFilled.empty());
-	for (auto & elem : allowedArtifacts)
-	{
-		if (elem->aClass == artifactClass)
-			listToBeFilled.push_back(elem);
-	}
-}
-
 void CArtHandler::afterLoadFinalization()
 {
 	//All artifacts have their id, so we can properly update their bonuses' source ids.

+ 5 - 11
lib/CArtHandler.h

@@ -172,21 +172,21 @@ public:
 class DLL_LINKAGE CArtHandler : public CHandlerBase<ArtifactID, Artifact, CArtifact, ArtifactService>
 {
 public:
-	std::vector<CArtifact*> treasures, minors, majors, relics; //tmp vectors!!! do not touch if you don't know what you are doing!!!
+	/// Stores number of times each artifact was placed on map via randomization
+	std::map<ArtifactID, int> allocatedArtifacts;
 
+	/// List of artifacts allowed on the map
 	std::vector<CArtifact *> allowedArtifacts;
-	std::set<ArtifactID> growingArtifacts;
 
 	void addBonuses(CArtifact *art, const JsonNode &bonusList);
 
-	void fillList(std::vector<CArtifact*> &listToBeFilled, CArtifact::EartClass artifactClass); //fills given empty list with allowed artifacts of given class. No side effects
-
 	static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor
 
 	/// Gets a artifact ID randomly and removes the selected artifact from this handler.
 	ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags);
 	ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function<bool(ArtifactID)> accepts);
 	ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function<bool(ArtifactID)> accepts);
+	ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::set<ArtifactID> filtered);
 
 	bool legalArtifact(const ArtifactID & id);
 	void initAllowedArtifactsList(const std::vector<bool> &allowed); //allowed[art_id] -> 0 if not allowed, 1 if allowed
@@ -207,11 +207,7 @@ public:
 	{
 		h & objects;
 		h & allowedArtifacts;
-		h & treasures;
-		h & minors;
-		h & majors;
-		h & relics;
-		h & growingArtifacts;
+		h & allocatedArtifacts;
 	}
 
 protected:
@@ -224,8 +220,6 @@ private:
 	void loadClass(CArtifact * art, const JsonNode & node) const;
 	void loadType(CArtifact * art, const JsonNode & node) const;
 	void loadComponents(CArtifact * art, const JsonNode & node);
-
-	void erasePickedArt(const ArtifactID & id);
 };
 
 struct DLL_LINKAGE ArtSlotInfo

+ 2 - 3
lib/CGameInfoCallback.cpp

@@ -607,7 +607,7 @@ EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, Bu
 	{
 		const TerrainTile *tile = getTile(t->bestLocation(), false);
 
-		if(!tile || tile->terType->isLand())
+		if(!tile || !tile->terType->isWater())
 			return EBuildingState::NO_WATER; //lack of water
 	}
 
@@ -942,7 +942,7 @@ bool CGameInfoCallback::isInTheMap(const int3 &pos) const
 
 void CGameInfoCallback::getVisibleTilesInRange(std::unordered_set<int3> &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula) const
 {
-	gs->getTilesInRange(tiles, pos, radious, *getPlayerID(), -1, distanceFormula);
+	gs->getTilesInRange(tiles, pos, radious, ETileVisibility::REVEALED, *getPlayerID(),  distanceFormula);
 }
 
 void CGameInfoCallback::calculatePaths(const std::shared_ptr<PathfinderConfig> & config)
@@ -955,7 +955,6 @@ void CGameInfoCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo &
 	gs->calculatePaths(hero, out);
 }
 
-
 const CArtifactInstance * CGameInfoCallback::getArtInstance( ArtifactInstanceID aid ) const
 {
 	return gs->map->artInstances[aid.num];

+ 1 - 0
lib/CGeneralTextHandler.cpp

@@ -475,6 +475,7 @@ CGeneralTextHandler::CGeneralTextHandler():
 	readToVector("core.cmpmusic", "DATA/CMPMUSIC.TXT" );
 	readToVector("core.minename", "DATA/MINENAME.TXT" );
 	readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" );
+	readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" );
 
 	static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT";
 	if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS)))

+ 1 - 1
lib/CSkillHandler.h

@@ -48,7 +48,7 @@ private:
 	std::string identifier;
 
 public:
-	CSkill(const SecondarySkill & id = SecondarySkill::DEFAULT, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false);
+	CSkill(const SecondarySkill & id = SecondarySkill::NONE, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false);
 	~CSkill() = default;
 
 	enum class Obligatory : ui8

+ 9 - 29
lib/IGameCallback.cpp

@@ -78,8 +78,8 @@ void CPrivilegedInfoCallback::getFreeTiles(std::vector<int3> & tiles) const
 void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set<int3> & tiles,
 											  const int3 & pos,
 											  int radious,
+											  ETileVisibility mode,
 											  std::optional<PlayerColor> player,
-											  int mode,
 											  int3::EDistanceFormula distanceFormula) const
 {
 	if(!!player && !player->isValidPlayer())
@@ -88,7 +88,7 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set<int3> & tiles,
 		return;
 	}
 	if(radious == CBuilding::HEIGHT_SKYSHIP) //reveal entire map
-		getAllTiles (tiles, player, -1, MapTerrainFilterMode::NONE);
+		getAllTiles (tiles, player, -1, [](auto * tile){return true;});
 	else
 	{
 		const TeamState * team = !player ? nullptr : gs->getPlayerTeam(*player);
@@ -97,13 +97,13 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set<int3> & tiles,
 			for (int yd = std::max<int>(pos.y - radious, 0); yd <= std::min<int>(pos.y + radious, gs->map->height - 1); yd++)
 			{
 				int3 tilePos(xd,yd,pos.z);
-				double distance = pos.dist(tilePos, distanceFormula);
+				int distance = pos.dist(tilePos, distanceFormula);
 
 				if(distance <= radious)
 				{
 					if(!player
-						|| (mode == 1  && (*team->fogOfWarMap)[pos.z][xd][yd] == 0)
-						|| (mode == -1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 1)
+						|| (mode == ETileVisibility::HIDDEN  && (*team->fogOfWarMap)[pos.z][xd][yd] == 0)
+						|| (mode == ETileVisibility::REVEALED && (*team->fogOfWarMap)[pos.z][xd][yd] == 1)
 					)
 						tiles.insert(int3(xd,yd,pos.z));
 				}
@@ -112,7 +112,7 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set<int3> & tiles,
 	}
 }
 
-void CPrivilegedInfoCallback::getAllTiles(std::unordered_set<int3> & tiles, std::optional<PlayerColor> Player, int level, MapTerrainFilterMode tileFilterMode) const
+void CPrivilegedInfoCallback::getAllTiles(std::unordered_set<int3> & tiles, std::optional<PlayerColor> Player, int level, std::function<bool(const TerrainTile *)> filter) const
 {
 	if(!!Player && !Player->isValidPlayer())
 	{
@@ -137,29 +137,9 @@ void CPrivilegedInfoCallback::getAllTiles(std::unordered_set<int3> & tiles, std:
 		{
 			for(int yd = 0; yd < gs->map->height; yd++)
 			{
-				bool isTileEligible = false;
-
-				switch(tileFilterMode)
-				{
-					case MapTerrainFilterMode::NONE:
-						isTileEligible = true;
-						break;
-					case MapTerrainFilterMode::WATER:
-						isTileEligible = getTile(int3(xd, yd, zd))->terType->isWater();
-						break;
-					case MapTerrainFilterMode::LAND:
-						isTileEligible = getTile(int3(xd, yd, zd))->terType->isLand();
-						break;
-					case MapTerrainFilterMode::LAND_CARTOGRAPHER:
-						isTileEligible = getTile(int3(xd, yd, zd))->terType->isSurfaceCartographerCompatible();
-						break;
-					case MapTerrainFilterMode::UNDERGROUND_CARTOGRAPHER:
-						isTileEligible = getTile(int3(xd, yd, zd))->terType->isUndergroundCartographerCompatible();
-						break;
-				}
-
-				if(isTileEligible)
-					tiles.insert(int3(xd, yd, zd));
+				int3 coordinates(xd, yd, zd);
+				if (filter(getTile(coordinates)))
+					tiles.insert(coordinates);
 			}
 		}
 	}

+ 5 - 15
lib/IGameCallback.h

@@ -43,15 +43,6 @@ namespace scripting
 class DLL_LINKAGE CPrivilegedInfoCallback : public CGameInfoCallback
 {
 public:
-	enum class MapTerrainFilterMode
-	{
-		NONE = 0,
-		LAND = 1,
-		WATER = 2,
-		LAND_CARTOGRAPHER = 3,
-		UNDERGROUND_CARTOGRAPHER = 4
-	};
-
 	CGameState *gameState();
 
 	//used for random spawns
@@ -60,14 +51,13 @@ public:
 	//mode 1 - only unrevealed tiles; mode 0 - all, mode -1 -  only revealed
 	void getTilesInRange(std::unordered_set<int3> & tiles,
 						 const int3 & pos,
-						 int radious,
+						 int radius,
+						 ETileVisibility mode,
 						 std::optional<PlayerColor> player = std::optional<PlayerColor>(),
-						 int mode = 0,
 						 int3::EDistanceFormula formula = int3::DIST_2D) const;
 
 	//returns all tiles on given level (-1 - both levels, otherwise number of level)
-	void getAllTiles(std::unordered_set<int3> &tiles, std::optional<PlayerColor> player = std::optional<PlayerColor>(),
-					 int level = -1, MapTerrainFilterMode tileFilterMode = MapTerrainFilterMode::NONE) const;
+	void getAllTiles(std::unordered_set<int3> &tiles, std::optional<PlayerColor> player, int level, std::function<bool(const TerrainTile *)> filter) const;
 
 	//gives 3 treasures, 3 minors, 1 major -> used by Black Market and Artifact Merchant
 	void pickAllowedArtsSet(std::vector<const CArtifact *> & out, CRandomGenerator & rand) const; 
@@ -135,8 +125,8 @@ public:
 	virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0;
 	virtual void sendAndApply(CPackForClient * pack) = 0;
 	virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map
-	virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) = 0;
-	virtual void changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player, bool hide) = 0;
+	virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) = 0;
+	virtual void changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode) = 0;
 	
 	virtual void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) = 0;
 };

+ 0 - 3
lib/JsonNode.cpp

@@ -753,10 +753,7 @@ std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonNode &ability)
 	{
 		// caller code can not handle this case and presumes that returned bonus is always valid
 		logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toJson());
-
 		b->type = BonusType::NONE;
-		assert(0); // or throw? Game *should* work with dummy bonus
-
 		return b;
 	}
 	return b;

+ 306 - 158
lib/JsonRandom.cpp

@@ -32,274 +32,415 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 namespace JsonRandom
 {
-	si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue)
+	si32 loadVariable(std::string variableGroup, const std::string & value, const Variables & variables, si32 defaultValue)
+	{
+		if (value.empty() || value[0] != '@')
+		{
+			logMod->warn("Invalid syntax in load value! Can not load value from '%s'", value);
+			return defaultValue;
+		}
+
+		std::string variableID = variableGroup + value;
+
+		if (variables.count(variableID) == 0)
+		{
+			logMod->warn("Invalid syntax in load value! Unknown variable '%s'", value);
+			return defaultValue;
+		}
+		return variables.at(variableID);
+	}
+
+	si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue)
 	{
 		if(value.isNull())
 			return defaultValue;
 		if(value.isNumber())
 			return static_cast<si32>(value.Float());
+		if (value.isString())
+			return loadVariable("number", value.String(), variables, defaultValue);
+
 		if(value.isVector())
 		{
 			const auto & vector = value.Vector();
 
 			size_t index= rng.getIntRange(0, vector.size()-1)();
-			return loadValue(vector[index], rng, 0);
+			return loadValue(vector[index], rng, variables, 0);
 		}
 		if(value.isStruct())
 		{
 			if (!value["amount"].isNull())
-				return static_cast<si32>(loadValue(value["amount"], rng, defaultValue));
-			si32 min = static_cast<si32>(loadValue(value["min"], rng, 0));
-			si32 max = static_cast<si32>(loadValue(value["max"], rng, 0));
+				return static_cast<si32>(loadValue(value["amount"], rng, variables, defaultValue));
+			si32 min = static_cast<si32>(loadValue(value["min"], rng, variables, 0));
+			si32 max = static_cast<si32>(loadValue(value["max"], rng, variables, 0));
 			return rng.getIntRange(min, max)();
 		}
 		return defaultValue;
 	}
 
-	std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set<std::string> & valuesSet)
+	template<typename IdentifierType>
+	IdentifierType decodeKey(const std::string & modScope, const std::string & value, const Variables & variables)
+	{
+		if (value.empty() || value[0] != '@')
+			return IdentifierType(*VLC->identifiers()->getIdentifier(modScope, IdentifierType::entityType(), value));
+		else
+			return loadVariable(IdentifierType::entityType(), value, variables, IdentifierType::NONE);
+	}
+
+	template<typename IdentifierType>
+	IdentifierType decodeKey(const JsonNode & value, const Variables & variables)
+	{
+		if (value.String().empty() || value.String()[0] != '@')
+			return IdentifierType(*VLC->identifiers()->getIdentifier(IdentifierType::entityType(), value));
+		else
+			return loadVariable(IdentifierType::entityType(), value.String(), variables, IdentifierType::NONE);
+	}
+
+	template<>
+	PlayerColor decodeKey(const JsonNode & value, const Variables & variables)
+	{
+		return PlayerColor(*VLC->identifiers()->getIdentifier("playerColor", value));
+	}
+
+	template<>
+	PrimarySkill decodeKey(const JsonNode & value, const Variables & variables)
+	{
+		return PrimarySkill(*VLC->identifiers()->getIdentifier("primarySkill", value));
+	}
+
+	template<>
+	PrimarySkill decodeKey(const std::string & modScope, const std::string & value, const Variables & variables)
+	{
+		if (value.empty() || value[0] != '@')
+			return PrimarySkill(*VLC->identifiers()->getIdentifier(modScope, "primarySkill", value));
+		else
+			return PrimarySkill(loadVariable("primarySkill", value, variables, static_cast<int>(PrimarySkill::NONE)));
+	}
+
+	/// Method that allows type-specific object filtering
+	/// Default implementation is to accept all input objects
+	template<typename IdentifierType>
+	std::set<IdentifierType> filterKeysTyped(const JsonNode & value, const std::set<IdentifierType> & valuesSet)
+	{
+		return valuesSet;
+	}
+
+	template<>
+	std::set<ArtifactID> filterKeysTyped(const JsonNode & value, const std::set<ArtifactID> & valuesSet)
+	{
+		assert(value.isStruct());
+
+		std::set<CArtifact::EartClass> allowedClasses;
+		std::set<ArtifactPosition> allowedPositions;
+		ui32 minValue = 0;
+		ui32 maxValue = std::numeric_limits<ui32>::max();
+
+		if (value["class"].getType() == JsonNode::JsonType::DATA_STRING)
+			allowedClasses.insert(CArtHandler::stringToClass(value["class"].String()));
+		else
+			for(const auto & entry : value["class"].Vector())
+				allowedClasses.insert(CArtHandler::stringToClass(entry.String()));
+
+		if (value["slot"].getType() == JsonNode::JsonType::DATA_STRING)
+			allowedPositions.insert(ArtifactPosition::decode(value["class"].String()));
+		else
+			for(const auto & entry : value["slot"].Vector())
+				allowedPositions.insert(ArtifactPosition::decode(entry.String()));
+
+		if (!value["minValue"].isNull())
+			minValue = static_cast<ui32>(value["minValue"].Float());
+		if (!value["maxValue"].isNull())
+			maxValue = static_cast<ui32>(value["maxValue"].Float());
+
+		std::set<ArtifactID> result;
+
+		for (auto const & artID : valuesSet)
+		{
+			CArtifact * art = VLC->arth->objects[artID];
+
+			if(!vstd::iswithin(art->getPrice(), minValue, maxValue))
+				continue;
+
+			if(!allowedClasses.empty() && !allowedClasses.count(art->aClass))
+				continue;
+
+			if(!IObjectInterface::cb->isAllowed(1, art->getIndex()))
+				continue;
+
+			if(!allowedPositions.empty())
+			{
+				bool positionAllowed = false;
+				for(const auto & pos : art->getPossibleSlots().at(ArtBearer::HERO))
+				{
+					if(allowedPositions.count(pos))
+						positionAllowed = true;
+				}
+
+				if (!positionAllowed)
+					continue;
+			}
+
+			result.insert(artID);
+		}
+		return result;
+	}
+
+	template<>
+	std::set<SpellID> filterKeysTyped(const JsonNode & value, const std::set<SpellID> & valuesSet)
+	{
+		std::set<SpellID> result = valuesSet;
+
+		if (!value["level"].isNull())
+		{
+			int32_t spellLevel = value["level"].Integer();
+
+			vstd::erase_if(result, [=](const SpellID & spell)
+			{
+				return VLC->spellh->getById(spell)->getLevel() != spellLevel;
+			});
+		}
+
+		if (!value["school"].isNull())
+		{
+			int32_t schoolID = VLC->identifiers()->getIdentifier("spellSchool", value["school"]).value();
+
+			vstd::erase_if(result, [=](const SpellID & spell)
+			{
+				return !VLC->spellh->getById(spell)->hasSchool(SpellSchool(schoolID));
+			});
+		}
+		return result;
+	}
+
+	template<typename IdentifierType>
+	std::set<IdentifierType> filterKeys(const JsonNode & value, const std::set<IdentifierType> & valuesSet, const Variables & variables)
 	{
 		if(value.isString())
-			return value.String();
-		
+			return { decodeKey<IdentifierType>(value, variables) };
+
+		assert(value.isStruct());
+
 		if(value.isStruct())
 		{
 			if(!value["type"].isNull())
-				return value["type"].String();
+				return filterKeys(value["type"], valuesSet, variables);
+
+			std::set<IdentifierType> filteredTypes = filterKeysTyped(value, valuesSet);
 
 			if(!value["anyOf"].isNull())
-				return RandomGeneratorUtil::nextItem(value["anyOf"].Vector(), rng)->String();
-			
+			{
+				std::set<IdentifierType> filteredAnyOf;
+				for (auto const & entry : value["anyOf"].Vector())
+				{
+					std::set<IdentifierType> subset = filterKeys(entry, valuesSet, variables);
+					filteredAnyOf.insert(subset.begin(), subset.end());
+				}
+
+				vstd::erase_if(filteredTypes, [&](const IdentifierType & value)
+				{
+					return filteredAnyOf.count(value) == 0;
+				});
+			}
+
 			if(!value["noneOf"].isNull())
 			{
-				auto copyValuesSet = valuesSet;
-				for(auto & s : value["noneOf"].Vector())
-					copyValuesSet.erase(s.String());
-				
-				if(!copyValuesSet.empty())
-					return *RandomGeneratorUtil::nextItem(copyValuesSet, rng);
+				for (auto const & entry : value["noneOf"].Vector())
+				{
+					std::set<IdentifierType> subset = filterKeys(entry, valuesSet, variables);
+					for (auto bannedEntry : subset )
+						filteredTypes.erase(bannedEntry);
+				}
 			}
+
+			return filteredTypes;
 		}
-		
-		return valuesSet.empty() ? "" : *RandomGeneratorUtil::nextItem(valuesSet, rng);
+		return valuesSet;
 	}
 
-	TResources loadResources(const JsonNode & value, CRandomGenerator & rng)
+	TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
 		TResources ret;
 
 		if (value.isVector())
 		{
 			for (const auto & entry : value.Vector())
-				ret += loadResource(entry, rng);
+				ret += loadResource(entry, rng, variables);
 			return ret;
 		}
 
 		for (size_t i=0; i<GameConstants::RESOURCE_QUANTITY; i++)
 		{
-			ret[i] = loadValue(value[GameConstants::RESOURCE_NAMES[i]], rng);
+			ret[i] = loadValue(value[GameConstants::RESOURCE_NAMES[i]], rng, variables);
 		}
 		return ret;
 	}
 
-	TResources loadResource(const JsonNode & value, CRandomGenerator & rng)
+	TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
-		std::set<std::string> defaultResources(std::begin(GameConstants::RESOURCE_NAMES), std::end(GameConstants::RESOURCE_NAMES) - 1); //except mithril
-		
-		std::string resourceName = loadKey(value, rng, defaultResources);
-		si32 resourceAmount = loadValue(value, rng, 0);
-		si32 resourceID(VLC->identifiers()->getIdentifier(value.meta, "resource", resourceName).value());
+		std::set<GameResID> defaultResources{
+			GameResID::WOOD,
+			GameResID::MERCURY,
+			GameResID::ORE,
+			GameResID::SULFUR,
+			GameResID::CRYSTAL,
+			GameResID::GEMS,
+			GameResID::GOLD
+		};
+
+		std::set<GameResID> potentialPicks = filterKeys(value, defaultResources, variables);
+		GameResID resourceID = *RandomGeneratorUtil::nextItem(potentialPicks, rng);
+		si32 resourceAmount = loadValue(value, rng, variables, 0);
 
 		TResources ret;
 		ret[resourceID] = resourceAmount;
 		return ret;
 	}
 
+	PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
+	{
+		std::set<PrimarySkill> defaultSkills{
+			PrimarySkill::ATTACK,
+			PrimarySkill::DEFENSE,
+			PrimarySkill::SPELL_POWER,
+			PrimarySkill::KNOWLEDGE
+		};
+		std::set<PrimarySkill> potentialPicks = filterKeys(value, defaultSkills, variables);
+		return *RandomGeneratorUtil::nextItem(potentialPicks, rng);
+	}
 
-	std::vector<si32> loadPrimary(const JsonNode & value, CRandomGenerator & rng)
+	std::vector<si32> loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
-		std::vector<si32> ret;
+		std::vector<si32> ret(GameConstants::PRIMARY_SKILLS, 0);
+		std::set<PrimarySkill> defaultSkills{
+			PrimarySkill::ATTACK,
+			PrimarySkill::DEFENSE,
+			PrimarySkill::SPELL_POWER,
+			PrimarySkill::KNOWLEDGE
+		};
+
 		if(value.isStruct())
 		{
-			for(const auto & name : NPrimarySkill::names)
+			for(const auto & pair : value.Struct())
 			{
-				ret.push_back(loadValue(value[name], rng));
+				PrimarySkill id = decodeKey<PrimarySkill>(pair.second.meta, pair.first, variables);
+				ret[static_cast<int>(id)] += loadValue(pair.second, rng, variables);
 			}
 		}
 		if(value.isVector())
 		{
-			ret.resize(GameConstants::PRIMARY_SKILLS, 0);
-			std::set<std::string> defaultStats(std::begin(NPrimarySkill::names), std::end(NPrimarySkill::names));
 			for(const auto & element : value.Vector())
 			{
-				auto key = loadKey(element, rng, defaultStats);
-				defaultStats.erase(key);
-				int id = vstd::find_pos(NPrimarySkill::names, key);
-				if(id != -1)
-					ret[id] += loadValue(element, rng);
+				std::set<PrimarySkill> potentialPicks = filterKeys(element, defaultSkills, variables);
+				PrimarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng);
+
+				defaultSkills.erase(skillID);
+				ret[static_cast<int>(skillID)] += loadValue(element, rng, variables);
 			}
 		}
 		return ret;
 	}
 
-	std::map<SecondarySkill, si32> loadSecondary(const JsonNode & value, CRandomGenerator & rng)
+	SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
+	{
+		std::set<SecondarySkill> defaultSkills;
+		for(const auto & skill : VLC->skillh->objects)
+			if (IObjectInterface::cb->isAllowed(2, skill->getIndex()))
+				defaultSkills.insert(skill->getId());
+
+		std::set<SecondarySkill> potentialPicks = filterKeys(value, defaultSkills, variables);
+		return *RandomGeneratorUtil::nextItem(potentialPicks, rng);
+	}
+
+	std::map<SecondarySkill, si32> loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
 		std::map<SecondarySkill, si32> ret;
 		if(value.isStruct())
 		{
 			for(const auto & pair : value.Struct())
 			{
-				SecondarySkill id(VLC->identifiers()->getIdentifier(pair.second.meta, "skill", pair.first).value());
-				ret[id] = loadValue(pair.second, rng);
+				SecondarySkill id = decodeKey<SecondarySkill>(pair.second.meta, pair.first, variables);
+				ret[id] = loadValue(pair.second, rng, variables);
 			}
 		}
 		if(value.isVector())
 		{
-			std::set<std::string> defaultSkills;
+			std::set<SecondarySkill> defaultSkills;
 			for(const auto & skill : VLC->skillh->objects)
-			{
-				IObjectInterface::cb->isAllowed(2, skill->getIndex());
-				auto scopeAndName = vstd::splitStringToPair(skill->getJsonKey(), ':');
-				if(scopeAndName.first == ModScope::scopeBuiltin() || scopeAndName.first == value.meta)
-					defaultSkills.insert(scopeAndName.second);
-				else
-					defaultSkills.insert(skill->getJsonKey());
-			}
-			
+				if (IObjectInterface::cb->isAllowed(2, skill->getIndex()))
+					defaultSkills.insert(skill->getId());
+
 			for(const auto & element : value.Vector())
 			{
-				auto key = loadKey(element, rng, defaultSkills);
-				defaultSkills.erase(key); //avoid dupicates
-				if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "skill", key))
-				{
-					SecondarySkill id(identifier.value());
-					ret[id] = loadValue(element, rng);
-				}
+				std::set<SecondarySkill> potentialPicks = filterKeys(element, defaultSkills, variables);
+				SecondarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng);
+
+				defaultSkills.erase(skillID); //avoid dupicates
+				ret[skillID] = loadValue(element, rng, variables);
 			}
 		}
 		return ret;
 	}
 
-	ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng)
+	ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
-		if (value.getType() == JsonNode::JsonType::DATA_STRING)
-			return ArtifactID(VLC->identifiers()->getIdentifier("artifact", value).value());
-
-		std::set<CArtifact::EartClass> allowedClasses;
-		std::set<ArtifactPosition> allowedPositions;
-		ui32 minValue = 0;
-		ui32 maxValue = std::numeric_limits<ui32>::max();
-
-		if (value["class"].getType() == JsonNode::JsonType::DATA_STRING)
-			allowedClasses.insert(CArtHandler::stringToClass(value["class"].String()));
-		else
-			for(const auto & entry : value["class"].Vector())
-				allowedClasses.insert(CArtHandler::stringToClass(entry.String()));
-
-		if (value["slot"].getType() == JsonNode::JsonType::DATA_STRING)
-			allowedPositions.insert(ArtifactPosition::decode(value["class"].String()));
-		else
-			for(const auto & entry : value["slot"].Vector())
-				allowedPositions.insert(ArtifactPosition::decode(entry.String()));
-
-		if (!value["minValue"].isNull()) minValue = static_cast<ui32>(value["minValue"].Float());
-		if (!value["maxValue"].isNull()) maxValue = static_cast<ui32>(value["maxValue"].Float());
+		std::set<ArtifactID> allowedArts;
+		for (auto const * artifact : VLC->arth->allowedArtifacts)
+			allowedArts.insert(artifact->getId());
 
-		return VLC->arth->pickRandomArtifact(rng, [=](const ArtifactID & artID) -> bool
-		{
-			CArtifact * art = VLC->arth->objects[artID];
-
-			if(!vstd::iswithin(art->getPrice(), minValue, maxValue))
-				return false;
+		std::set<ArtifactID> potentialPicks = filterKeys(value, allowedArts, variables);
 
-			if(!allowedClasses.empty() && !allowedClasses.count(art->aClass))
-				return false;
-			
-			if(!IObjectInterface::cb->isAllowed(1, art->getIndex()))
-				return false;
-
-			if(!allowedPositions.empty())
-			{
-				for(const auto & pos : art->getPossibleSlots().at(ArtBearer::HERO))
-				{
-					if(allowedPositions.count(pos))
-						return true;
-				}
-				return false;
-			}
-			return true;
-		});
+		return VLC->arth->pickRandomArtifact(rng, potentialPicks);
 	}
 
-	std::vector<ArtifactID> loadArtifacts(const JsonNode & value, CRandomGenerator & rng)
+	std::vector<ArtifactID> loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
 		std::vector<ArtifactID> ret;
 		for (const JsonNode & entry : value.Vector())
 		{
-			ret.push_back(loadArtifact(entry, rng));
+			ret.push_back(loadArtifact(entry, rng, variables));
 		}
 		return ret;
 	}
 
-	SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector<SpellID> spells)
+	SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
-		if (value.getType() == JsonNode::JsonType::DATA_STRING)
-			return SpellID(VLC->identifiers()->getIdentifier("spell", value).value());
+		std::set<SpellID> defaultSpells;
+		for(const auto & spell : VLC->spellh->objects)
+			if (IObjectInterface::cb->isAllowed(0, spell->getIndex()))
+				defaultSpells.insert(spell->getId());
 
-		if (!value["level"].isNull())
-		{
-			int32_t spellLevel = value["level"].Float();
+		std::set<SpellID> potentialPicks = filterKeys(value, defaultSpells, variables);
 
-			vstd::erase_if(spells, [=](const SpellID & spell)
-			{
-				return VLC->spellh->getById(spell)->getLevel() != spellLevel;
-			});
-		}
-
-		if (!value["school"].isNull())
-		{
-			int32_t schoolID = VLC->identifiers()->getIdentifier("spellSchool", value["school"]).value();
-
-			vstd::erase_if(spells, [=](const SpellID & spell)
-			{
-				return !VLC->spellh->getById(spell)->hasSchool(SpellSchool(schoolID));
-			});
-		}
-
-		if (spells.empty())
+		if (potentialPicks.empty())
 		{
 			logMod->warn("Failed to select suitable random spell!");
 			return SpellID::NONE;
 		}
-		return SpellID(*RandomGeneratorUtil::nextItem(spells, rng));
+		return *RandomGeneratorUtil::nextItem(potentialPicks, rng);
 	}
 
-	std::vector<SpellID> loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector<SpellID> & spells)
+	std::vector<SpellID> loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
 		std::vector<SpellID> ret;
 		for (const JsonNode & entry : value.Vector())
 		{
-			ret.push_back(loadSpell(entry, rng, spells));
+			ret.push_back(loadSpell(entry, rng, variables));
 		}
 		return ret;
 	}
 
-	std::vector<PlayerColor> loadColors(const JsonNode & value, CRandomGenerator & rng)
+	std::vector<PlayerColor> loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
 		std::vector<PlayerColor> ret;
-		std::set<std::string> def;
-		
-		for(auto & color : GameConstants::PLAYER_COLOR_NAMES)
-			def.insert(color);
-		
+		std::set<PlayerColor> defaultPlayers;
+		for(size_t i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
+			defaultPlayers.insert(PlayerColor(i));
+
 		for(auto & entry : value.Vector())
 		{
-			auto key = loadKey(entry, rng, def);
-			auto pos = vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, key);
-			if(pos < 0)
-				logMod->warn("Unable to determine player color %s", key);
-			else
-				ret.emplace_back(pos);
+			std::set<PlayerColor> potentialPicks = filterKeys(entry, defaultPlayers, variables);
+			ret.push_back(*RandomGeneratorUtil::nextItem(potentialPicks, rng));
 		}
+
 		return ret;
 	}
 
@@ -323,11 +464,25 @@ namespace JsonRandom
 		return ret;
 	}
 
-	CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng)
+	CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
 		CStackBasicDescriptor stack;
-		stack.type = VLC->creh->objects[VLC->identifiers()->getIdentifier("creature", value["type"]).value()];
-		stack.count = loadValue(value, rng);
+
+		std::set<CreatureID> defaultCreatures;
+		for(const auto & creature : VLC->creh->objects)
+			if (!creature->special)
+				defaultCreatures.insert(creature->getId());
+
+		std::set<CreatureID> potentialPicks = filterKeys(value, defaultCreatures, variables);
+		CreatureID pickedCreature;
+
+		if (!potentialPicks.empty())
+			pickedCreature = *RandomGeneratorUtil::nextItem(potentialPicks, rng);
+		else
+			logMod->warn("Failed to select suitable random creature!");
+
+		stack.type = VLC->creh->objects[pickedCreature];
+		stack.count = loadValue(value, rng, variables);
 		if (!value["upgradeChance"].isNull() && !stack.type->upgrades.empty())
 		{
 			if (int(value["upgradeChance"].Float()) > rng.nextInt(99)) // select random upgrade
@@ -338,17 +493,17 @@ namespace JsonRandom
 		return stack;
 	}
 
-	std::vector<CStackBasicDescriptor> loadCreatures(const JsonNode & value, CRandomGenerator & rng)
+	std::vector<CStackBasicDescriptor> loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
 		std::vector<CStackBasicDescriptor> ret;
 		for (const JsonNode & node : value.Vector())
 		{
-			ret.push_back(loadCreature(node, rng));
+			ret.push_back(loadCreature(node, rng, variables));
 		}
 		return ret;
 	}
 
-	std::vector<RandomStackInfo> evaluateCreatures(const JsonNode & value)
+	std::vector<RandomStackInfo> evaluateCreatures(const JsonNode & value, const Variables & variables)
 	{
 		std::vector<RandomStackInfo> ret;
 		for (const JsonNode & node : value.Vector())
@@ -374,13 +529,6 @@ namespace JsonRandom
 		return ret;
 	}
 
-	//std::vector<Component> loadComponents(const JsonNode & value)
-	//{
-	//	std::vector<Component> ret;
-	//	return ret;
-	//	//TODO
-	//}
-
 	std::vector<Bonus> DLL_LINKAGE loadBonuses(const JsonNode & value)
 	{
 		std::vector<Bonus> ret;

+ 18 - 15
lib/JsonRandom.h

@@ -24,6 +24,8 @@ class CStackBasicDescriptor;
 
 namespace JsonRandom
 {
+	using Variables = std::map<std::string, int>;
+
 	struct DLL_LINKAGE RandomStackInfo
 	{
 		std::vector<const CCreature *> allowedCreatures;
@@ -31,29 +33,30 @@ namespace JsonRandom
 		si32 maxAmount;
 	};
 
-	DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue = 0);
-	DLL_LINKAGE std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set<std::string> & valuesSet = {});
-	DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng);
-	DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng);
-	DLL_LINKAGE std::vector<si32> loadPrimary(const JsonNode & value, CRandomGenerator & rng);
-	DLL_LINKAGE std::map<SecondarySkill, si32> loadSecondary(const JsonNode & value, CRandomGenerator & rng);
+	DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue = 0);
+
+	DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
+	DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
+	DLL_LINKAGE PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
+	DLL_LINKAGE std::vector<si32> loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
+	DLL_LINKAGE SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
+	DLL_LINKAGE std::map<SecondarySkill, si32> loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
 
-	DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng);
-	DLL_LINKAGE std::vector<ArtifactID> loadArtifacts(const JsonNode & value, CRandomGenerator & rng);
+	DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
+	DLL_LINKAGE std::vector<ArtifactID> loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
 
-	DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector<SpellID> spells = {});
-	DLL_LINKAGE std::vector<SpellID> loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector<SpellID> & spells = {});
+	DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
+	DLL_LINKAGE std::vector<SpellID> loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
 
-	DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng);
-	DLL_LINKAGE std::vector<CStackBasicDescriptor> loadCreatures(const JsonNode & value, CRandomGenerator & rng);
-	DLL_LINKAGE std::vector<RandomStackInfo> evaluateCreatures(const JsonNode & value);
+	DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
+	DLL_LINKAGE std::vector<CStackBasicDescriptor> loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
+	DLL_LINKAGE std::vector<RandomStackInfo> evaluateCreatures(const JsonNode & value, const Variables & variables);
 
-	DLL_LINKAGE std::vector<PlayerColor> loadColors(const JsonNode & value, CRandomGenerator & rng);
+	DLL_LINKAGE std::vector<PlayerColor> loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables);
 	DLL_LINKAGE std::vector<HeroTypeID> loadHeroes(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE std::vector<HeroClassID> loadHeroClasses(const JsonNode & value, CRandomGenerator & rng);
 
 	DLL_LINKAGE std::vector<Bonus> loadBonuses(const JsonNode & value);
-	//DLL_LINKAGE std::vector<Component> loadComponents(const JsonNode & value);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 3 - 6
lib/NetPacks.h

@@ -348,7 +348,7 @@ struct DLL_LINKAGE FoWChange : public CPackForClient
 
 	std::unordered_set<int3> tiles;
 	PlayerColor player;
-	ui8 mode = 0; //mode==0 - hide, mode==1 - reveal
+	ETileVisibility mode;
 	bool waitForDialogs = false;
 
 	virtual void visitTyped(ICPackVisitor & visitor) override;
@@ -556,17 +556,14 @@ struct DLL_LINKAGE AddQuest : public CPackForClient
 
 struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient
 {
-	std::vector<CArtifact *> treasures, minors, majors, relics;
+	std::map<ArtifactID, int> allocatedArtifacts;
 
 	void applyGs(CGameState * gs) const;
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
-		h & treasures;
-		h & minors;
-		h & majors;
-		h & relics;
+		h & allocatedArtifacts;
 	}
 };
 

+ 5 - 7
lib/NetPacksLib.cpp

@@ -853,10 +853,7 @@ void AddQuest::applyGs(CGameState * gs) const
 
 void UpdateArtHandlerLists::applyGs(CGameState * gs) const
 {
-	VLC->arth->minors = minors;
-	VLC->arth->majors = majors;
-	VLC->arth->treasures = treasures;
-	VLC->arth->relics = relics;
+	VLC->arth->allocatedArtifacts = allocatedArtifacts;
 }
 
 void UpdateMapEvents::applyGs(CGameState * gs) const
@@ -932,8 +929,9 @@ void FoWChange::applyGs(CGameState *gs)
 	TeamState * team = gs->getPlayerTeam(player);
 	auto fogOfWarMap = team->fogOfWarMap;
 	for(const int3 & t : tiles)
-		(*fogOfWarMap)[t.z][t.x][t.y] = mode;
-	if (mode == 0) //do not hide too much
+		(*fogOfWarMap)[t.z][t.x][t.y] = mode != ETileVisibility::HIDDEN;
+
+	if (mode == ETileVisibility::HIDDEN) //do not hide too much
 	{
 		std::unordered_set<int3> tilesRevealed;
 		for (auto & elem : gs->map->objects)
@@ -948,7 +946,7 @@ void FoWChange::applyGs(CGameState *gs)
 				case Obj::TOWN:
 				case Obj::ABANDONED_MINE:
 					if(vstd::contains(team->players, o->tempOwner)) //check owned observators
-						gs->getTilesInRange(tilesRevealed, o->getSightCenter(), o->getSightRadius(), o->tempOwner, 1);
+						gs->getTilesInRange(tilesRevealed, o->getSightCenter(), o->getSightRadius(), ETileVisibility::HIDDEN, o->tempOwner);
 					break;
 				}
 			}

+ 6 - 11
lib/TerrainHandler.cpp

@@ -155,9 +155,14 @@ bool TerrainType::isWater() const
 	return passabilityType & PassabilityType::WATER;
 }
 
+bool TerrainType::isRock() const
+{
+	return passabilityType & PassabilityType::ROCK;
+}
+
 bool TerrainType::isPassable() const
 {
-	return !(passabilityType & PassabilityType::ROCK);
+	return !isRock();
 }
 
 bool TerrainType::isSurface() const
@@ -170,16 +175,6 @@ bool TerrainType::isUnderground() const
 	return passabilityType & PassabilityType::SUBTERRANEAN;
 }
 
-bool TerrainType::isSurfaceCartographerCompatible() const
-{
-	return isSurface();
-}
-
-bool TerrainType::isUndergroundCartographerCompatible() const
-{
-	return isLand() && isPassable() && !isSurface();
-}
-
 bool TerrainType::isTransitionRequired() const
 {
 	return transitionRequired;

+ 3 - 2
lib/TerrainHandler.h

@@ -84,12 +84,13 @@ public:
 
 	bool isLand() const;
 	bool isWater() const;
+	bool isRock() const;
+
 	bool isPassable() const;
+
 	bool isSurface() const;
 	bool isUnderground() const;
 	bool isTransitionRequired() const;
-	bool isSurfaceCartographerCompatible() const;
-	bool isUndergroundCartographerCompatible() const;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{

+ 10 - 0
lib/constants/EntityIdentifiers.cpp

@@ -323,4 +323,14 @@ const ObstacleInfo * Obstacle::getInfo() const
 	return VLC->obstacles()->getById(*this);
 }
 
+std::string GameResID::entityType()
+{
+	return "resource";
+}
+
+std::string SecondarySkill::entityType()
+{
+	return "secondarySkill";
+}
+
 VCMI_LIB_NAMESPACE_END

+ 5 - 3
lib/constants/EntityIdentifiers.h

@@ -308,8 +308,7 @@ class SecondarySkillBase : public IdentifierBase
 public:
 	enum Type : int32_t
 	{
-		WRONG = -2,
-		DEFAULT = -1,
+		NONE = -1,
 		PATHFINDING = 0,
 		ARCHERY,
 		LOGISTICS,
@@ -347,6 +346,7 @@ class SecondarySkill : public IdentifierWithEnum<SecondarySkill, SecondarySkillB
 {
 public:
 	using IdentifierWithEnum<SecondarySkill, SecondarySkillBase>::IdentifierWithEnum;
+	static std::string entityType();
 };
 
 class DLL_LINKAGE FactionID : public Identifier<FactionID>
@@ -937,7 +937,7 @@ public:
 		COUNT,
 
 		WOOD_AND_ORE = 127,  // special case for town bonus resource
-		INVALID = -1
+		NONE = -1
 	};
 };
 
@@ -945,6 +945,8 @@ class GameResID : public IdentifierWithEnum<GameResID, GameResIDBase>
 {
 public:
 	using IdentifierWithEnum<GameResID, GameResIDBase>::IdentifierWithEnum;
+
+	static std::string entityType();
 };
 
 // Deprecated

+ 6 - 0
lib/constants/Enumerations.h

@@ -250,4 +250,10 @@ enum class EBattleResult : int8_t
 	SURRENDER = 2,
 };
 
+enum class ETileVisibility : int8_t // Fog of war change
+{
+	HIDDEN,
+	REVEALED
+};
+
 VCMI_LIB_NAMESPACE_END

+ 1 - 1
lib/gameState/CGameState.cpp

@@ -942,7 +942,7 @@ void CGameState::initFogOfWar()
 			if(!obj || !vstd::contains(elem.second.players, obj->tempOwner)) continue; //not a flagged object
 
 			std::unordered_set<int3> tiles;
-			getTilesInRange(tiles, obj->getSightCenter(), obj->getSightRadius(), obj->tempOwner, 1);
+			getTilesInRange(tiles, obj->getSightCenter(), obj->getSightRadius(), ETileVisibility::HIDDEN, obj->tempOwner);
 			for(const int3 & tile : tiles)
 			{
 				(*elem.second.fogOfWarMap)[tile.z][tile.x][tile.y] = 1;

+ 1 - 1
lib/int3.h

@@ -110,7 +110,7 @@ public:
 		switch(formula)
 		{
 		case DIST_2D:
-			return static_cast<ui32>(dist2d(o));
+			return std::round(dist2d(o));
 		case DIST_MANHATTAN:
 			return static_cast<ui32>(mandist2d(o));
 		case DIST_CHEBYSHEV:

+ 15 - 11
lib/mapObjectConstructors/CBankInstanceConstructor.cpp

@@ -37,17 +37,15 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input)
 BankConfig CBankInstanceConstructor::generateConfig(const JsonNode & level, CRandomGenerator & rng) const
 {
 	BankConfig bc;
+	JsonRandom::Variables emptyVariables;
 
 	bc.chance = static_cast<ui32>(level["chance"].Float());
-	bc.guards = JsonRandom::loadCreatures(level["guards"], rng);
-
-	std::vector<SpellID> spells;
-	IObjectInterface::cb->getAllowedSpells(spells);
+	bc.guards = JsonRandom::loadCreatures(level["guards"], rng, emptyVariables);
 
 	bc.resources = ResourceSet(level["reward"]["resources"]);
-	bc.creatures = JsonRandom::loadCreatures(level["reward"]["creatures"], rng);
-	bc.artifacts = JsonRandom::loadArtifacts(level["reward"]["artifacts"], rng);
-	bc.spells = JsonRandom::loadSpells(level["reward"]["spells"], rng, spells);
+	bc.creatures = JsonRandom::loadCreatures(level["reward"]["creatures"], rng, emptyVariables);
+	bc.artifacts = JsonRandom::loadArtifacts(level["reward"]["artifacts"], rng, emptyVariables);
+	bc.spells = JsonRandom::loadSpells(level["reward"]["spells"], rng, emptyVariables);
 
 	return bc;
 }
@@ -105,10 +103,12 @@ static void addStackToArmy(IObjectInfo::CArmyStructure & army, const CCreature *
 
 IObjectInfo::CArmyStructure CBankInfo::minGuards() const
 {
+	JsonRandom::Variables emptyVariables;
+
 	std::vector<IObjectInfo::CArmyStructure> armies;
 	for(auto configEntry : config)
 	{
-		auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"]);
+		auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"], emptyVariables);
 		IObjectInfo::CArmyStructure army;
 		for(auto & stack : stacks)
 		{
@@ -126,10 +126,12 @@ IObjectInfo::CArmyStructure CBankInfo::minGuards() const
 
 IObjectInfo::CArmyStructure CBankInfo::maxGuards() const
 {
+	JsonRandom::Variables emptyVariables;
+
 	std::vector<IObjectInfo::CArmyStructure> armies;
 	for(auto configEntry : config)
 	{
-		auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"]);
+		auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"], emptyVariables);
 		IObjectInfo::CArmyStructure army;
 		for(auto & stack : stacks)
 		{
@@ -147,12 +149,13 @@ IObjectInfo::CArmyStructure CBankInfo::maxGuards() const
 
 TPossibleGuards CBankInfo::getPossibleGuards() const
 {
+	JsonRandom::Variables emptyVariables;
 	TPossibleGuards out;
 
 	for(const JsonNode & configEntry : config)
 	{
 		const JsonNode & guardsInfo = configEntry["guards"];
-		auto stacks = JsonRandom::evaluateCreatures(guardsInfo);
+		auto stacks = JsonRandom::evaluateCreatures(guardsInfo, emptyVariables);
 		IObjectInfo::CArmyStructure army;
 
 
@@ -187,12 +190,13 @@ std::vector<PossibleReward<TResources>> CBankInfo::getPossibleResourcesReward()
 
 std::vector<PossibleReward<CStackBasicDescriptor>> CBankInfo::getPossibleCreaturesReward() const
 {
+	JsonRandom::Variables emptyVariables;
 	std::vector<PossibleReward<CStackBasicDescriptor>> aproximateReward;
 
 	for(const JsonNode & configEntry : config)
 	{
 		const JsonNode & guardsInfo = configEntry["reward"]["creatures"];
-		auto stacks = JsonRandom::evaluateCreatures(guardsInfo);
+		auto stacks = JsonRandom::evaluateCreatures(guardsInfo, emptyVariables);
 
 		for(auto stack : stacks)
 		{

+ 0 - 6
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -26,7 +26,6 @@
 #include "../mapObjectConstructors/DwellingInstanceConstructor.h"
 #include "../mapObjectConstructors/HillFortInstanceConstructor.h"
 #include "../mapObjectConstructors/ShipyardInstanceConstructor.h"
-#include "../mapObjectConstructors/ShrineInstanceConstructor.h"
 #include "../mapObjects/CGCreature.h"
 #include "../mapObjects/CGPandoraBox.h"
 #include "../mapObjects/CQuest.h"
@@ -54,7 +53,6 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER_CLASS("bank", CBankInstanceConstructor);
 	SET_HANDLER_CLASS("boat", BoatInstanceConstructor);
 	SET_HANDLER_CLASS("market", MarketInstanceConstructor);
-	SET_HANDLER_CLASS("shrine", ShrineInstanceConstructor);
 	SET_HANDLER_CLASS("hillFort", HillFortInstanceConstructor);
 	SET_HANDLER_CLASS("shipyard", ShipyardInstanceConstructor);
 	SET_HANDLER_CLASS("monster", CreatureInstanceConstructor);
@@ -71,7 +69,6 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER("randomDwelling", CGDwelling);
 
 	SET_HANDLER("generic", CGObjectInstance);
-	SET_HANDLER("cartographer", CCartographer);
 	SET_HANDLER("artifact", CGArtifact);
 	SET_HANDLER("borderGate", CGBorderGate);
 	SET_HANDLER("borderGuard", CGBorderGuard);
@@ -84,18 +81,15 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER("magi", CGMagi);
 	SET_HANDLER("mine", CGMine);
 	SET_HANDLER("obelisk", CGObelisk);
-	SET_HANDLER("observatory", CGObservatory);
 	SET_HANDLER("pandora", CGPandoraBox);
 	SET_HANDLER("prison", CGHeroInstance);
 	SET_HANDLER("questGuard", CGQuestGuard);
-	SET_HANDLER("scholar", CGScholar);
 	SET_HANDLER("seerHut", CGSeerHut);
 	SET_HANDLER("sign", CGSignBottle);
 	SET_HANDLER("siren", CGSirens);
 	SET_HANDLER("monolith", CGMonolith);
 	SET_HANDLER("subterraneanGate", CGSubterraneanGate);
 	SET_HANDLER("whirlpool", CGWhirlpool);
-	SET_HANDLER("witch", CGWitchHut);
 	SET_HANDLER("terrain", CGTerrainPatch);
 
 #undef SET_HANDLER_CLASS

+ 1 - 2
lib/mapObjectConstructors/CRewardableConstructor.h

@@ -35,8 +35,7 @@ public:
 	{
 		AObjectTypeHandler::serialize(h, version);
 
-		if (version >= 816)
-			h & objectInfo;
+		h & objectInfo;
 	}
 };
 

+ 3 - 1
lib/mapObjectConstructors/CommonConstructors.cpp

@@ -256,9 +256,11 @@ void MarketInstanceConstructor::initializeObject(CGMarket * market) const
 
 void MarketInstanceConstructor::randomizeObject(CGMarket * object, CRandomGenerator & rng) const
 {
+	JsonRandom::Variables emptyVariables;
+
 	if(auto * university = dynamic_cast<CGUniversity *>(object))
 	{
-		for(auto skill : JsonRandom::loadSecondary(predefinedOffer, rng))
+		for(auto skill : JsonRandom::loadSecondaries(predefinedOffer, rng, emptyVariables))
 			university->skills.push_back(skill.first.getNum());
 	}
 }

+ 2 - 1
lib/mapObjectConstructors/DwellingInstanceConstructor.cpp

@@ -93,7 +93,8 @@ void DwellingInstanceConstructor::randomizeObject(CGDwelling * object, CRandomGe
 	}
 	else if(guards.getType() == JsonNode::JsonType::DATA_VECTOR) //custom guards (eg. Elemental Conflux)
 	{
-		for(auto & stack : JsonRandom::loadCreatures(guards, rng))
+		JsonRandom::Variables emptyVariables;
+		for(auto & stack : JsonRandom::loadCreatures(guards, rng, emptyVariables))
 		{
 			dwelling->putStack(SlotID(dwelling->stacksCount()), new CStackInstance(stack.type->getId(), stack.count));
 		}

+ 0 - 42
lib/mapObjectConstructors/ShrineInstanceConstructor.cpp

@@ -1,42 +0,0 @@
-/*
-* ShrineInstanceConstructor.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 "ShrineInstanceConstructor.h"
-
-#include "../mapObjects/MiscObjects.h"
-#include "../JsonRandom.h"
-#include "../IGameCallback.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-void ShrineInstanceConstructor::initTypeData(const JsonNode & config)
-{
-	parameters = config;
-}
-
-void ShrineInstanceConstructor::randomizeObject(CGShrine * shrine, CRandomGenerator & rng) const
-{
-	auto visitTextParameter = parameters["visitText"];
-
-	if (visitTextParameter.isNumber())
-		shrine->visitText.appendLocalString(EMetaText::ADVOB_TXT, static_cast<ui32>(visitTextParameter.Float()));
-	else
-		shrine->visitText.appendRawString(visitTextParameter.String());
-
-	if(shrine->spell == SpellID::NONE) // shrine has no predefined spell
-	{
-		std::vector<SpellID> possibilities;
-		shrine->cb->getAllowedSpells(possibilities);
-
-		shrine->spell =JsonRandom::loadSpell(parameters["spell"], rng, possibilities);
-	}
-}
-
-VCMI_LIB_NAMESPACE_END

+ 0 - 34
lib/mapObjectConstructors/ShrineInstanceConstructor.h

@@ -1,34 +0,0 @@
-/*
-* ShrineInstanceConstructor.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 "CDefaultObjectTypeHandler.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CGShrine;
-
-class ShrineInstanceConstructor final : public CDefaultObjectTypeHandler<CGShrine>
-{
-	JsonNode parameters;
-
-protected:
-	void initTypeData(const JsonNode & config) override;
-	void randomizeObject(CGShrine * object, CRandomGenerator & rng) const override;
-
-public:
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<AObjectTypeHandler&>(*this);
-		h & parameters;
-	}
-};
-
-VCMI_LIB_NAMESPACE_END

+ 37 - 22
lib/mapObjects/CBank.cpp

@@ -18,6 +18,7 @@
 #include "../CGeneralTextHandler.h"
 #include "../CSoundBase.h"
 #include "../GameSettings.h"
+#include "../CPlayerState.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../mapObjectConstructors/CBankInstanceConstructor.h"
 #include "../IGameCallback.h"
@@ -51,8 +52,37 @@ bool CBank::isCoastVisitable() const
 
 std::string CBank::getHoverText(PlayerColor player) const
 {
-	// TODO: record visited players
-	return getObjectName() + " " + visitedTxt(bc == nullptr);
+	if (!wasVisited(player))
+		return getObjectName();
+
+	return getObjectName() + "\n" + visitedTxt(bc == nullptr);
+}
+
+std::vector<Component> CBank::getPopupComponents(PlayerColor player) const
+{
+	if (!wasVisited(player))
+		return {};
+
+	if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION))
+		return {};
+
+	std::map<CreatureID, int> guardsAmounts;
+	std::vector<Component> result;
+
+	for (auto const & slot : Slots())
+		if (slot.second)
+			guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount();
+
+	for (auto const & guard : guardsAmounts)
+	{
+		Component comp;
+		comp.id = Component::EComponentType::CREATURE;
+		comp.subtype = guard.first.getNum();
+		comp.val = guard.second;
+
+		result.push_back(comp);
+	}
+	return result;
 }
 
 void CBank::setConfig(const BankConfig & config)
@@ -98,11 +128,14 @@ void CBank::newTurn(CRandomGenerator & rand) const
 
 bool CBank::wasVisited (PlayerColor player) const
 {
-	return !bc; //FIXME: player A should not know about visit done by player B
+	return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id));
 }
 
 void CBank::onHeroVisit(const CGHeroInstance * h) const
 {
+	ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id);
+	cb->sendAndApply(&cov);
+
 	int banktext = 0;
 	switch (ID)
 	{
@@ -130,28 +163,10 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const
 	bd.player = h->getOwner();
 	bd.soundID = soundBase::invalid; // Sound is handled in json files, else two sounds are played
 	bd.text.appendLocalString(EMetaText::ADVOB_TXT, banktext);
+	bd.components = getPopupComponents(h->getOwner());
 	if (banktext == 32)
 		bd.text.replaceRawString(getObjectName());
 
-	if (VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION))
-	{
-		std::map<CreatureID, int> guardsAmounts;
-
-		for (auto const & slot : Slots())
-			if (slot.second)
-				guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount();
-
-		for (auto const & guard : guardsAmounts)
-		{
-			Component comp;
-			comp.id = Component::EComponentType::CREATURE;
-			comp.subtype = guard.first.getNum();
-			comp.val = guard.second;
-
-			bd.components.push_back(comp);
-		}
-	}
-
 	cb->showBlockingDialog(&bd);
 }
 

+ 2 - 0
lib/mapObjects/CBank.h

@@ -41,6 +41,8 @@ public:
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
 	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
 
+	std::vector<Component> getPopupComponents(PlayerColor player) const override;
+
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & static_cast<CArmedInstance&>(*this);

+ 7 - 7
lib/mapObjects/CGHeroInstance.cpp

@@ -272,7 +272,7 @@ CGHeroInstance::CGHeroInstance():
 {
 	setNodeType(HERO);
 	ID = Obj::HERO;
-	secSkills.emplace_back(SecondarySkill::DEFAULT, -1);
+	secSkills.emplace_back(SecondarySkill::NONE, -1);
 	blockVisit = true;
 }
 
@@ -334,7 +334,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand)
 			pushPrimSkill(static_cast<PrimarySkill>(g), type->heroClass->primarySkillInitial[g]);
 		}
 	}
-	if(secSkills.size() == 1 && secSkills[0] == std::pair<SecondarySkill,ui8>(SecondarySkill::DEFAULT, -1)) //set secondary skills to default
+	if(secSkills.size() == 1 && secSkills[0] == std::pair<SecondarySkill,ui8>(SecondarySkill::NONE, -1)) //set secondary skills to default
 		secSkills = type->secSkillsInit;
 
 	if (gender == EHeroGender::DEFAULT)
@@ -789,9 +789,9 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const
 	}
 }
 
-bool CGHeroInstance::canLearnSpell(const spells::Spell * spell) const
+bool CGHeroInstance::canLearnSpell(const spells::Spell * spell, bool allowBanned) const
 {
-    if(!hasSpellbook())
+	if(!hasSpellbook())
 		return false;
 
 	if(spell->getLevel() > maxSpellLevel()) //not enough wisdom
@@ -812,7 +812,7 @@ bool CGHeroInstance::canLearnSpell(const spells::Spell * spell) const
 		return false;//creature abilities can not be learned
 	}
 
-	if(!IObjectInterface::cb->isAllowed(0, spell->getIndex()))
+	if(!allowBanned && !IObjectInterface::cb->isAllowed(0, spell->getIndex()))
 	{
 		logGlobal->warn("Hero %s try to learn banned spell %s", nodeName(), spell->getNameTranslated());
 		return false;//banned spells should not be learned
@@ -1598,7 +1598,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
 		bool normalSkills = false;
 		for(const auto & p : secSkills)
 		{
-			if(p.first == SecondarySkill(SecondarySkill::DEFAULT))
+			if(p.first == SecondarySkill(SecondarySkill::NONE))
 				defaultSkills = true;
 			else
 				normalSkills = true;
@@ -1636,7 +1636,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
 		secSkills.clear();
 		if(secondarySkills.getType() == JsonNode::JsonType::DATA_NULL)
 		{
-			secSkills.emplace_back(SecondarySkill::DEFAULT, -1);
+			secSkills.emplace_back(SecondarySkill::NONE, -1);
 		}
 		else
 		{

+ 1 - 1
lib/mapObjects/CGHeroInstance.h

@@ -175,7 +175,7 @@ public:
 	int getCurrentLuck(int stack=-1, bool town=false) const;
 	int32_t getSpellCost(const spells::Spell * sp) const; //do not use during battles -> bonuses from army would be ignored
 
-	bool canLearnSpell(const spells::Spell * spell) const;
+	bool canLearnSpell(const spells::Spell * spell,  bool allowBanned = false) const;
 	bool canCastThisSpell(const spells::Spell * spell) const; //determines if this hero can cast given spell; takes into account existing spell in spellbook, existing spellbook and artifact bonuses
 
 	/// convert given position between map position (CGObjectInstance::pos) and visitable position used for hero interactions

+ 19 - 0
lib/mapObjects/CGObjectInstance.cpp

@@ -271,6 +271,25 @@ std::string CGObjectInstance::getHoverText(const CGHeroInstance * hero) const
 	return getHoverText(hero->tempOwner);
 }
 
+std::string CGObjectInstance::getPopupText(PlayerColor player) const
+{
+	return getHoverText(player);
+}
+std::string CGObjectInstance::getPopupText(const CGHeroInstance * hero) const
+{
+	return getHoverText(hero);
+}
+
+std::vector<Component> CGObjectInstance::getPopupComponents(PlayerColor player) const
+{
+	return {};
+}
+
+std::vector<Component> CGObjectInstance::getPopupComponents(const CGHeroInstance * hero) const
+{
+	return getPopupComponents(hero->getOwner());
+}
+
 void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const
 {
 	switch(ID)

+ 6 - 0
lib/mapObjects/CGObjectInstance.h

@@ -112,6 +112,12 @@ public:
 	/// Returns hero-specific hover name, including visited/not visited info. Default = player-specific name
 	virtual std::string getHoverText(const CGHeroInstance * hero) const;
 
+	virtual std::string getPopupText(PlayerColor player) const;
+	virtual std::string getPopupText(const CGHeroInstance * hero) const;
+
+	virtual std::vector<Component> getPopupComponents(PlayerColor player) const;
+	virtual std::vector<Component> getPopupComponents(const CGHeroInstance * hero) const;
+
 	/** OVERRIDES OF IObjectInterface **/
 
 	void initObj(CRandomGenerator & rand) override;

+ 2 - 0
lib/mapObjects/CGTownBuilding.cpp

@@ -396,6 +396,8 @@ bool CTownRewardableBuilding::wasVisitedBefore(const CGHeroInstance * contextHer
 			return contextHero->hasBonusFrom(BonusSource::TOWN_STRUCTURE, Bonus::getSid32(town->town->faction->getIndex(), bID));
 		case Rewardable::VISIT_HERO:
 			return visitors.find(contextHero->id) != visitors.end();
+		case Rewardable::VISIT_LIMITER:
+			return configuration.visitLimiter.heroAllowed(contextHero);
 		default:
 			return false;
 	}

+ 1 - 5
lib/mapObjects/CGTownInstance.cpp

@@ -1066,11 +1066,7 @@ void CGTownInstance::battleFinished(const CGHeroInstance * hero, const BattleRes
 void CGTownInstance::onTownCaptured(const PlayerColor & winner) const
 {
 	setOwner(winner);
-	FoWChange fw;
-	fw.player = winner;
-	fw.mode = 1;
-	cb->getTilesInRange(fw.tiles, getSightCenter(), getSightRadius(), winner, 1);
-	cb->sendAndApply(& fw);
+	cb->changeFogOfWar(getSightCenter(), getSightRadius(), winner, ETileVisibility::REVEALED);
 }
 
 void CGTownInstance::afterAddToMap(CMap * map)

+ 121 - 20
lib/mapObjects/CRewardableObject.cpp

@@ -21,13 +21,6 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-// FIXME: copy-pasted from CObjectHandler
-static std::string visitedTxt(const bool visited)
-{
-	int id = visited ? 352 : 353;
-	return VLC->generaltexth->allTexts[id];
-}
-
 void CRewardableObject::grantRewardWithMessage(const CGHeroInstance * contextHero, int index, bool markAsVisit) const
 {
 	auto vi = configuration.info.at(index);
@@ -54,15 +47,28 @@ void CRewardableObject::selectRewardWthMessage(const CGHeroInstance * contextHer
 	BlockingDialog sd(configuration.canRefuse, rewardIndices.size() > 1);
 	sd.player = contextHero->tempOwner;
 	sd.text = dialog;
+	sd.components = loadComponents(contextHero, rewardIndices);
+	cb->showBlockingDialog(&sd);
+}
 
-	if (rewardIndices.size() > 1)
-		for (auto index : rewardIndices)
-			sd.components.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero));
+std::vector<Component> CRewardableObject::loadComponents(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices) const
+{
+	std::vector<Component> result;
 
-	if (rewardIndices.size() == 1)
-		configuration.info.at(rewardIndices.front()).reward.loadComponents(sd.components, contextHero);
+	if (rewardIndices.empty())
+		return result;
 
-	cb->showBlockingDialog(&sd);
+	if (configuration.selectMode != Rewardable::SELECT_FIRST)
+	{
+		for (auto index : rewardIndices)
+			result.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero));
+	}
+	else
+	{
+		configuration.info.at(rewardIndices.front()).reward.loadComponents(result, contextHero);
+	}
+
+	return result;
 }
 
 void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
@@ -124,6 +130,12 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
 	{
 		logGlobal->debug("Revisiting already visited object");
 
+		if (!wasVisited(h->getOwner()))
+		{
+			ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id);
+			cb->sendAndApply(&cov);
+		}
+
 		auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED);
 		if (!visitedRewards.empty())
 			grantRewardWithMessage(h, visitedRewards[0], false);
@@ -188,6 +200,8 @@ bool CRewardableObject::wasVisitedBefore(const CGHeroInstance * contextHero) con
 			return contextHero->hasBonusFrom(BonusSource::OBJECT, ID);
 		case Rewardable::VISIT_HERO:
 			return contextHero->visitedObjects.count(ObjectInstanceID(id));
+		case Rewardable::VISIT_LIMITER:
+			return configuration.visitLimiter.heroAllowed(contextHero);
 		default:
 			return false;
 	}
@@ -200,6 +214,7 @@ bool CRewardableObject::wasVisited(PlayerColor player) const
 		case Rewardable::VISIT_UNLIMITED:
 		case Rewardable::VISIT_BONUS:
 		case Rewardable::VISIT_HERO:
+		case Rewardable::VISIT_LIMITER:
 			return false;
 		case Rewardable::VISIT_ONCE:
 		case Rewardable::VISIT_PLAYER:
@@ -209,6 +224,11 @@ bool CRewardableObject::wasVisited(PlayerColor player) const
 	}
 }
 
+bool CRewardableObject::wasScouted(PlayerColor player) const
+{
+	return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id));
+}
+
 bool CRewardableObject::wasVisited(const CGHeroInstance * h) const
 {
 	switch (configuration.visitMode)
@@ -217,23 +237,104 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const
 			return h->hasBonusFrom(BonusSource::OBJECT, ID);
 		case Rewardable::VISIT_HERO:
 			return h->visitedObjects.count(ObjectInstanceID(id));
+		case Rewardable::VISIT_LIMITER:
+			return wasScouted(h->getOwner()) && configuration.visitLimiter.heroAllowed(h);
 		default:
-			return wasVisited(h->tempOwner);
+			return wasVisited(h->getOwner());
+	}
+}
+
+std::string CRewardableObject::getDisplayTextImpl(PlayerColor player, const CGHeroInstance * hero, bool includeDescription) const
+{
+	std::string result = getObjectName();
+
+	if (includeDescription && !getDescriptionMessage(player, hero).empty())
+		result += "\n" + getDescriptionMessage(player, hero);
+
+	if (hero)
+	{
+		if(configuration.visitMode != Rewardable::VISIT_UNLIMITED)
+		{
+			if (wasVisited(hero))
+				result += "\n" + configuration.visitedTooltip.toString();
+			else
+				result += "\n " + configuration.notVisitedTooltip.toString();
+		}
+	}
+	else
+	{
+		if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE)
+		{
+			if (wasVisited(player))
+				result += "\n" + configuration.visitedTooltip.toString();
+			else
+				result += "\n" + configuration.notVisitedTooltip.toString();
+		}
 	}
+	return result;
 }
 
 std::string CRewardableObject::getHoverText(PlayerColor player) const
 {
-	if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE)
-		return getObjectName() + " " + visitedTxt(wasVisited(player));
-	return getObjectName();
+	return getDisplayTextImpl(player, nullptr, false);
 }
 
 std::string CRewardableObject::getHoverText(const CGHeroInstance * hero) const
 {
-	if(configuration.visitMode != Rewardable::VISIT_UNLIMITED)
-		return getObjectName() + " " + visitedTxt(wasVisited(hero));
-	return getObjectName();
+	return getDisplayTextImpl(hero->getOwner(), hero, false);
+}
+
+std::string CRewardableObject::getPopupText(PlayerColor player) const
+{
+	return getDisplayTextImpl(player, nullptr, true);
+}
+
+std::string CRewardableObject::getPopupText(const CGHeroInstance * hero) const
+{
+	return getDisplayTextImpl(hero->getOwner(), hero, true);
+}
+
+std::string CRewardableObject::getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const
+{
+	if (!wasScouted(player) || configuration.info.empty())
+		return configuration.description.toString();
+
+	auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
+	if (rewardIndices.empty() && !configuration.info[0].description.empty())
+		return configuration.info[0].description.toString();
+
+	if (!configuration.info[rewardIndices.front()].description.empty())
+		return configuration.info[rewardIndices.front()].description.toString();
+
+	return configuration.description.toString();
+}
+
+std::vector<Component> CRewardableObject::getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const
+{
+	if (!wasScouted(player))
+		return {};
+
+	if (!configuration.showScoutedPreview)
+		return {};
+
+	auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
+	if (rewardIndices.empty() && !configuration.info.empty())
+		rewardIndices.push_back(0);
+
+	if (rewardIndices.empty())
+		return {};
+
+	return loadComponents(hero, rewardIndices);
+}
+
+std::vector<Component> CRewardableObject::getPopupComponents(PlayerColor player) const
+{
+	return getPopupComponentsImpl(player, nullptr);
+}
+
+std::vector<Component> CRewardableObject::getPopupComponents(const CGHeroInstance * hero) const
+{
+	return getPopupComponentsImpl(hero->getOwner(), hero);
 }
 
 void CRewardableObject::setPropertyDer(ui8 what, ui32 val)

+ 16 - 7
lib/mapObjects/CRewardableObject.h

@@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class DLL_LINKAGE CRewardableObject : public CArmedInstance, public Rewardable::Interface
 {
 protected:
-	
+
 	bool onceVisitableObjectCleared = false;
 	
 	/// reward selected by player, no serialize
@@ -37,10 +37,19 @@ protected:
 	virtual void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const;
 	virtual void selectRewardWthMessage(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices, const MetaString & dialog) const;
 
+	std::vector<Component> loadComponents(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices) const;
+
+	std::string getDisplayTextImpl(PlayerColor player, const CGHeroInstance * hero, bool includeDescription) const;
+	std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const;
+	std::vector<Component> getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const;
+
 public:
 	/// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player)
 	bool wasVisited(PlayerColor player) const override;
 	bool wasVisited(const CGHeroInstance * h) const override;
+
+	/// Returns true if object was scouted by player and he is aware of its internal state
+	bool wasScouted(PlayerColor player) const;
 	
 	/// gives reward to player or ask for choice in case of multiple rewards
 	void onHeroVisit(const CGHeroInstance *h) const override;
@@ -63,6 +72,12 @@ public:
 	std::string getHoverText(PlayerColor player) const override;
 	std::string getHoverText(const CGHeroInstance * hero) const override;
 
+	std::string getPopupText(PlayerColor player) const override;
+	std::string getPopupText(const CGHeroInstance * hero) const override;
+
+	std::vector<Component> getPopupComponents(PlayerColor player) const override;
+	std::vector<Component> getPopupComponents(const CGHeroInstance * hero) const override;
+
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & static_cast<CArmedInstance&>(*this);
@@ -74,10 +89,6 @@ public:
 //TODO:
 
 // MAX
-// class DLL_LINKAGE CGPandoraBox : public CArmedInstance
-// class DLL_LINKAGE CGEvent : public CGPandoraBox  //event objects
-// class DLL_LINKAGE CGSeerHut : public CArmedInstance, public IQuestObject //army is used when giving reward
-// class DLL_LINKAGE CGQuestGuard : public CGSeerHut
 // class DLL_LINKAGE CBank : public CArmedInstance
 // class DLL_LINKAGE CGPyramid : public CBank
 
@@ -90,7 +101,5 @@ public:
 
 // POSSIBLE
 // class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles
-// class DLL_LINKAGE CGWitchHut : public CPlayersVisited
-// class DLL_LINKAGE CGScholar : public CGObjectInstance
 
 VCMI_LIB_NAMESPACE_END

+ 2 - 398
lib/mapObjects/MiscObjects.cpp

@@ -842,203 +842,6 @@ void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler)
 	}
 }
 
-void CGWitchHut::initObj(CRandomGenerator & rand)
-{
-	if (allowedAbilities.empty()) //this can happen for RMG and RoE maps.
-	{
-		auto defaultAllowed = VLC->skillh->getDefaultAllowed();
-
-		// Necromancy and Leadership can't be learned by default
-		defaultAllowed[SecondarySkill::NECROMANCY] = false;
-		defaultAllowed[SecondarySkill::LEADERSHIP] = false;
-
-		for(int i = 0; i < defaultAllowed.size(); i++)
-			if (defaultAllowed[i] && cb->isAllowed(2, i))
-				allowedAbilities.insert(SecondarySkill(i));
-	}
-	ability = *RandomGeneratorUtil::nextItem(allowedAbilities, rand);
-}
-
-void CGWitchHut::onHeroVisit( const CGHeroInstance * h ) const
-{
-	InfoWindow iw;
-	iw.type = EInfoWindowMode::AUTO;
-	iw.player = h->getOwner();
-	if(!wasVisited(h->tempOwner))
-		cb->setObjProperty(id, CGWitchHut::OBJPROP_VISITED, h->tempOwner.getNum());
-	ui32 txt_id;
-	if(h->getSecSkillLevel(SecondarySkill(ability))) //you already know this skill
-	{
-		txt_id =172;
-	}
-	else if(!h->canLearnSkill()) //already all skills slots used
-	{
-		txt_id = 173;
-	}
-	else //give sec skill
-	{
-		iw.components.emplace_back(Component::EComponentType::SEC_SKILL, ability, 1, 0);
-		txt_id = 171;
-		cb->changeSecSkill(h, SecondarySkill(ability), 1, true);
-	}
-
-	iw.text.appendLocalString(EMetaText::ADVOB_TXT,txt_id);
-	iw.text.replaceLocalString(EMetaText::SEC_SKILL_NAME, ability);
-	cb->showInfoDialog(&iw);
-}
-
-std::string CGWitchHut::getHoverText(PlayerColor player) const
-{
-	std::string hoverName = getObjectName();
-	if(wasVisited(player))
-	{
-		hoverName += "\n" + VLC->generaltexth->allTexts[356]; // + (learn %s)
-		boost::algorithm::replace_first(hoverName, "%s", VLC->skillh->getByIndex(ability)->getNameTranslated());
-	}
-	return hoverName;
-}
-
-std::string CGWitchHut::getHoverText(const CGHeroInstance * hero) const
-{
-	std::string hoverName = getHoverText(hero->tempOwner);
-	if(wasVisited(hero->tempOwner) && hero->getSecSkillLevel(SecondarySkill(ability))) //hero knows that ability
-		hoverName += "\n\n" + VLC->generaltexth->allTexts[357]; // (Already learned)
-	return hoverName;
-}
-
-void CGWitchHut::serializeJsonOptions(JsonSerializeFormat & handler)
-{
-	//TODO: unify allowed abilities with others - make them std::vector<bool>
-
-	std::vector<bool> temp;
-	size_t skillCount = VLC->skillh->size();
-	temp.resize(skillCount, false);
-
-	auto standard = VLC->skillh->getDefaultAllowed(); //todo: for WitchHut default is all except Leadership and Necromancy
-
-	if(handler.saving)
-	{
-		for(SecondarySkill i(0); i < SecondarySkill(skillCount); ++i)
-			if(vstd::contains(allowedAbilities, i))
-				temp[i] = true;
-	}
-
-	handler.serializeLIC("allowedSkills", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, standard, temp);
-
-	if(!handler.saving)
-	{
-		allowedAbilities.clear();
-		for(si32 i = 0; i < skillCount; ++i)
-			if(temp[i])
-				allowedAbilities.insert(SecondarySkill(i));
-	}
-}
-
-void CGObservatory::onHeroVisit( const CGHeroInstance * h ) const
-{
-	InfoWindow iw;
-	iw.type = EInfoWindowMode::AUTO;
-	iw.player = h->tempOwner;
-	switch (ID)
-	{
-	case Obj::REDWOOD_OBSERVATORY:
-	case Obj::PILLAR_OF_FIRE:
-	{
-		iw.text.appendLocalString(EMetaText::ADVOB_TXT,98 + (ID==Obj::PILLAR_OF_FIRE));
-
-		FoWChange fw;
-		fw.player = h->tempOwner;
-		fw.mode = 1;
-		cb->getTilesInRange (fw.tiles, pos, 20, h->tempOwner, 1);
-		cb->sendAndApply (&fw);
-		break;
-	}
-	case Obj::COVER_OF_DARKNESS:
-	{
-		iw.text.appendLocalString (EMetaText::ADVOB_TXT, 31);
-		for (auto & player : cb->gameState()->players)
-		{
-			if (cb->getPlayerStatus(player.first) == EPlayerStatus::INGAME &&
-				cb->getPlayerRelations(player.first, h->tempOwner) == PlayerRelations::ENEMIES)
-				cb->changeFogOfWar(visitablePos(), 20, player.first, true);
-		}
-		break;
-	}
-	}
-	cb->showInfoDialog(&iw);
-}
-
-void CGShrine::onHeroVisit( const CGHeroInstance * h ) const
-{
-	if(spell == SpellID::NONE)
-	{
-		logGlobal->error("Not initialized shrine visited!");
-		return;
-	}
-
-	if(!wasVisited(h->tempOwner))
-		cb->setObjProperty(id, CGShrine::OBJPROP_VISITED, h->tempOwner.getNum());
-
-	InfoWindow iw;
-	iw.type = EInfoWindowMode::AUTO;
-	iw.player = h->getOwner();
-	iw.text = visitText;
-	iw.text.appendLocalString(EMetaText::SPELL_NAME,spell);
-	iw.text.appendRawString(".");
-
-	if(!h->getArt(ArtifactPosition::SPELLBOOK))
-	{
-		iw.text.appendLocalString(EMetaText::ADVOB_TXT,131);
-	}
-	else if(h->spellbookContainsSpell(spell))//hero already knows the spell
-	{
-		iw.text.appendLocalString(EMetaText::ADVOB_TXT,174);
-	}
-	else if(spell.toSpell()->getLevel() > h->maxSpellLevel()) //it's third level spell and hero doesn't have wisdom
-	{
-		iw.text.appendLocalString(EMetaText::ADVOB_TXT,130);
-	}
-	else //give spell
-	{
-		std::set<SpellID> spells;
-		spells.insert(spell);
-		cb->changeSpells(h, true, spells);
-
-		iw.components.emplace_back(Component::EComponentType::SPELL, spell, 0, 0);
-	}
-
-	cb->showInfoDialog(&iw);
-}
-
-void CGShrine::initObj(CRandomGenerator & rand)
-{
-	VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand);
-}
-
-std::string CGShrine::getHoverText(PlayerColor player) const
-{
-	std::string hoverName = getObjectName();
-	if(wasVisited(player))
-	{
-		hoverName += "\n" + VLC->generaltexth->allTexts[355]; // + (learn %s)
-		boost::algorithm::replace_first(hoverName,"%s", spell.toSpell()->getNameTranslated());
-	}
-	return hoverName;
-}
-
-std::string CGShrine::getHoverText(const CGHeroInstance * hero) const
-{
-	std::string hoverName = getHoverText(hero->tempOwner);
-	if(wasVisited(hero->tempOwner) && hero->spellbookContainsSpell(spell)) //know what spell there is and hero knows that spell
-		hoverName += "\n\n" + VLC->generaltexth->allTexts[354]; // (Already learned)
-	return hoverName;
-}
-
-void CGShrine::serializeJsonOptions(JsonSerializeFormat & handler)
-{
-	handler.serializeId("spell", spell, SpellID::NONE);
-}
-
 void CGSignBottle::initObj(CRandomGenerator & rand)
 {
 	//if no text is set than we pick random from the predefined ones
@@ -1071,132 +874,6 @@ void CGSignBottle::serializeJsonOptions(JsonSerializeFormat& handler)
 	handler.serializeStruct("text", message);
 }
 
-void CGScholar::onHeroVisit( const CGHeroInstance * h ) const
-{
-	EBonusType type = bonusType;
-	int bid = bonusID;
-	//check if the bonus if applicable, if not - give primary skill (always possible)
-	int ssl = h->getSecSkillLevel(SecondarySkill(bid)); //current sec skill level, used if bonusType == 1
-	if((type == SECONDARY_SKILL	&& ((ssl == 3)  ||  (!ssl  &&  !h->canLearnSkill()))) ////hero already has expert level in the skill or (don't know skill and doesn't have free slot)
-		|| (type == SPELL && !h->canLearnSpell(SpellID(bid).toSpell())))
-	{
-		type = PRIM_SKILL;
-		bid = CRandomGenerator::getDefault().nextInt(GameConstants::PRIMARY_SKILLS - 1);
-	}
-
-	InfoWindow iw;
-	iw.type = EInfoWindowMode::AUTO;
-	iw.player = h->getOwner();
-	iw.text.appendLocalString(EMetaText::ADVOB_TXT,115);
-
-	switch (type)
-	{
-	case PRIM_SKILL:
-		cb->changePrimSkill(h,static_cast<PrimarySkill>(bid),+1);
-		iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, bid, +1, 0);
-		break;
-	case SECONDARY_SKILL:
-		cb->changeSecSkill(h,SecondarySkill(bid),+1);
-		iw.components.emplace_back(Component::EComponentType::SEC_SKILL, bid, ssl + 1, 0);
-		break;
-	case SPELL:
-		{
-			std::set<SpellID> hlp;
-			hlp.insert(SpellID(bid));
-			cb->changeSpells(h,true,hlp);
-			iw.components.emplace_back(Component::EComponentType::SPELL, bid, 0, 0);
-		}
-		break;
-	default:
-		logGlobal->error("Error: wrong bonus type (%d) for Scholar!\n", static_cast<int>(type));
-		return;
-	}
-
-	cb->showInfoDialog(&iw);
-	cb->removeObject(this, h->getOwner());
-}
-
-void CGScholar::initObj(CRandomGenerator & rand)
-{
-	blockVisit = true;
-	if(bonusType == RANDOM)
-	{
-		bonusType = static_cast<EBonusType>(rand.nextInt(2));
-		switch(bonusType)
-		{
-		case PRIM_SKILL:
-			bonusID = rand.nextInt(GameConstants::PRIMARY_SKILLS -1);
-			break;
-		case SECONDARY_SKILL:
-			bonusID = rand.nextInt(static_cast<int>(VLC->skillh->size()) - 1);
-			break;
-		case SPELL:
-			std::vector<SpellID> possibilities;
-			cb->getAllowedSpells (possibilities);
-			bonusID = *RandomGeneratorUtil::nextItem(possibilities, rand);
-			break;
-		}
-	}
-}
-
-void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler)
-{
-	if(handler.saving)
-	{
-		std::string value;
-		switch(bonusType)
-		{
-		case PRIM_SKILL:
-			value = NPrimarySkill::names[bonusID];
-			handler.serializeString("rewardPrimSkill", value);
-			break;
-		case SECONDARY_SKILL:
-			value = CSkillHandler::encodeSkill(bonusID);
-			handler.serializeString("rewardSkill", value);
-			break;
-		case SPELL:
-			value = SpellID::encode(bonusID);
-			handler.serializeString("rewardSpell", value);
-			break;
-		case RANDOM:
-			break;
-		}
-	}
-	else
-	{
-		//TODO: unify
-		const JsonNode & json = handler.getCurrent();
-		bonusType = RANDOM;
-		if(!json["rewardPrimSkill"].String().empty())
-		{
-			auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "primSkill", json["rewardPrimSkill"].String());
-			if(raw)
-			{
-				bonusType = PRIM_SKILL;
-				bonusID = raw.value();
-			}
-		}
-		else if(!json["rewardSkill"].String().empty())
-		{
-			auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "skill", json["rewardSkill"].String());
-			if(raw)
-			{
-				bonusType = SECONDARY_SKILL;
-				bonusID = raw.value();
-			}
-		}
-		else if(!json["rewardSpell"].String().empty())
-		{
-			auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "spell", json["rewardSpell"].String());
-			if(raw)
-			{
-				bonusType = SPELL;
-				bonusID = raw.value();
-			}
-		}
-	}
-}
-
 void CGGarrison::onHeroVisit (const CGHeroInstance *h) const
 {
 	auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner);
@@ -1267,14 +944,14 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const
 
 			FoWChange fw;
 			fw.player = h->tempOwner;
-			fw.mode = 1;
+			fw.mode = ETileVisibility::REVEALED;
 			fw.waitForDialogs = true;
 
 			for(const auto & it : eyelist[subID])
 			{
 				const CGObjectInstance *eye = cb->getObj(it);
 
-				cb->getTilesInRange (fw.tiles, eye->pos, 10, h->tempOwner, 1);
+				cb->getTilesInRange (fw.tiles, eye->pos, 10, ETileVisibility::HIDDEN, h->tempOwner);
 				cb->sendAndApply(&fw);
 				cv.pos = eye->pos;
 
@@ -1413,79 +1090,6 @@ BoatId CGShipyard::getBoatType() const
 	return createdBoat;
 }
 
-void CCartographer::onHeroVisit( const CGHeroInstance * h ) const
-{
-	//if player has not bought map of this subtype yet and underground exist for stalagmite cartographer
-	if (!wasVisited(h->getOwner()) && (subID != 2 || cb->gameState()->map->twoLevel))
-	{
-		if (cb->getResource(h->tempOwner, EGameResID::GOLD) >= 1000) //if he can afford a map
-		{
-			//ask if he wants to buy one
-			int text=0;
-			switch (subID)
-			{
-				case 0:
-					text = 25;
-					break;
-				case 1:
-					text = 26;
-					break;
-				case 2:
-					text = 27;
-					break;
-				default:
-					logGlobal->warn("Unrecognized subtype of cartographer");
-			}
-			assert(text);
-			BlockingDialog bd (true, false);
-			bd.player = h->getOwner();
-			bd.text.appendLocalString (EMetaText::ADVOB_TXT, text);
-			cb->showBlockingDialog (&bd);
-		}
-		else //if he cannot afford
-		{
-			h->showInfoDialog(28);
-		}
-	}
-	else //if he already visited carographer
-	{
-		h->showInfoDialog(24);
-	}
-}
-
-void CCartographer::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
-{
-	if(answer) //if hero wants to buy map
-	{
-		cb->giveResource(hero->tempOwner, EGameResID::GOLD, -1000);
-		FoWChange fw;
-		fw.mode = 1;
-		fw.player = hero->tempOwner;
-
-		//subIDs of different types of cartographers:
-		//water = 0; land = 1; underground = 2;
-
-		IGameCallback::MapTerrainFilterMode tileFilterMode = IGameCallback::MapTerrainFilterMode::NONE;
-
-		switch(subID)
-		{
-			case 0:
-				tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::WATER;
-				break;
-			case 1:
-				tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::LAND_CARTOGRAPHER;
-				break;
-			case 2:
-				tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::UNDERGROUND_CARTOGRAPHER;
-				break;
-		}
-
-		cb->getAllTiles(fw.tiles, hero->tempOwner, -1, tileFilterMode); //reveal appropriate tiles
-		cb->sendAndApply(&fw);
-		cb->setObjProperty(id, CCartographer::OBJPROP_VISITED, hero->tempOwner.getNum());
-	}
-}
-
 void CGDenOfthieves::onHeroVisit (const CGHeroInstance * h) const
 {
 	cb->showObjectWindow(this, EOpenWindowMode::THIEVES_GUILD, h, false);

+ 0 - 85
lib/mapObjects/MiscObjects.h

@@ -57,46 +57,6 @@ protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
 };
 
-class DLL_LINKAGE CGWitchHut : public CTeamVisited
-{
-public:
-	std::set<SecondarySkill> allowedAbilities;
-	SecondarySkill ability;
-
-	std::string getHoverText(PlayerColor player) const override;
-	std::string getHoverText(const CGHeroInstance * hero) const override;
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	void initObj(CRandomGenerator & rand) override;
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CTeamVisited&>(*this);
-		h & allowedAbilities;
-		h & ability;
-	}
-protected:
-	void serializeJsonOptions(JsonSerializeFormat & handler) override;
-};
-
-class DLL_LINKAGE CGScholar : public CGObjectInstance
-{
-public:
-	enum EBonusType {PRIM_SKILL, SECONDARY_SKILL, SPELL, RANDOM = 255};
-	EBonusType bonusType;
-	ui16 bonusID; //ID of skill/spell
-
-	CGScholar() : bonusType(EBonusType::RANDOM),bonusID(0){};
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	void initObj(CRandomGenerator & rand) override;
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CGObjectInstance&>(*this);
-		h & bonusType;
-		h & bonusID;
-	}
-protected:
-	void serializeJsonOptions(JsonSerializeFormat & handler) override;
-};
-
 class DLL_LINKAGE CGGarrison : public CArmedInstance
 {
 public:
@@ -169,27 +129,6 @@ protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
 };
 
-class DLL_LINKAGE CGShrine : public CTeamVisited
-{
-public:
-	MetaString visitText;
-	SpellID spell; //id of spell or NONE if random
-
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	void initObj(CRandomGenerator & rand) override;
-	std::string getHoverText(PlayerColor player) const override;
-	std::string getHoverText(const CGHeroInstance * hero) const override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CTeamVisited&>(*this);;
-		h & spell;
-		h & visitText;
-	}
-protected:
-	void serializeJsonOptions(JsonSerializeFormat & handler) override;
-};
-
 class DLL_LINKAGE CGMine : public CArmedInstance
 {
 public:
@@ -334,17 +273,6 @@ public:
 	}
 };
 
-class DLL_LINKAGE CGObservatory : public CGObjectInstance //Redwood observatory
-{
-public:
-	void onHeroVisit(const CGHeroInstance * h) const override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CGObjectInstance&>(*this);
-	}
-};
-
 class DLL_LINKAGE CGBoat : public CGObjectInstance, public CBonusSystemNode
 {
 public:
@@ -416,19 +344,6 @@ public:
 	}
 };
 
-class DLL_LINKAGE CCartographer : public CTeamVisited
-{
-///behaviour varies depending on surface and  floor
-public:
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CTeamVisited&>(*this);
-	}
-};
-
 class DLL_LINKAGE CGDenOfthieves : public CGObjectInstance
 {
 	void onHeroVisit(const CGHeroInstance * h) const override;

+ 101 - 17
lib/mapping/MapFormatH3M.cpp

@@ -27,6 +27,7 @@
 #include "../TerrainHandler.h"
 #include "../TextOperations.h"
 #include "../VCMI_Lib.h"
+#include "../constants/StringConstants.h"
 #include "../filesystem/CBinaryReader.h"
 #include "../filesystem/Filesystem.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
@@ -34,6 +35,7 @@
 #include "../mapObjects/CGCreature.h"
 #include "../mapObjects/MapObjects.h"
 #include "../mapObjects/ObjectTemplate.h"
+#include "../modding/ModScope.h"
 #include "../spells/CSpellHandler.h"
 
 #include <boost/crc.hpp>
@@ -1139,32 +1141,102 @@ CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition)
 	return object;
 }
 
-CGObjectInstance * CMapLoaderH3M::readWitchHut()
+CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate)
 {
-	auto * object = new CGWitchHut();
+	auto * object = readGeneric(position, objectTemplate);
+	auto * rewardable = dynamic_cast<CRewardableObject*>(object);
+
+	assert(rewardable);
 
 	// AB and later maps have allowed abilities defined in H3M
 	if(features.levelAB)
 	{
-		reader->readBitmaskSkills(object->allowedAbilities, false);
+		std::set<SecondarySkill> allowedAbilities;
+		reader->readBitmaskSkills(allowedAbilities, false);
 
-		if(object->allowedAbilities.size() != 1)
+		if(allowedAbilities.size() != 1)
 		{
 			auto defaultAllowed = VLC->skillh->getDefaultAllowed();
 
-			for(int skillID = 0; skillID < VLC->skillh->size(); ++skillID)
+			for(int skillID = features.skillsCount; skillID < defaultAllowed.size(); ++skillID)
 				if(defaultAllowed[skillID])
-					object->allowedAbilities.insert(SecondarySkill(skillID));
+					allowedAbilities.insert(SecondarySkill(skillID));
 		}
+
+		JsonVector anyOfList;
+
+		for (auto const & skill : allowedAbilities)
+		{
+			JsonNode entry;
+			entry.String() = VLC->skills()->getById(skill)->getJsonKey();
+			anyOfList.push_back(entry);
+		}
+		JsonNode variable;
+		variable["anyOf"].Vector() = anyOfList;
+		variable.setMeta(ModScope::scopeGame()); // list may include skills from all mods
+
+		rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable);
 	}
 	return object;
 }
 
-CGObjectInstance * CMapLoaderH3M::readScholar()
+CGObjectInstance * CMapLoaderH3M::readScholar(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate)
 {
-	auto * object = new CGScholar();
-	object->bonusType = static_cast<CGScholar::EBonusType>(reader->readUInt8());
-	object->bonusID = reader->readUInt8();
+	enum class ScholarBonusType : uint8_t {
+		PRIM_SKILL = 0,
+		SECONDARY_SKILL = 1,
+		SPELL = 2,
+		RANDOM = 255
+	};
+
+	auto * object = readGeneric(position, objectTemplate);
+	auto * rewardable = dynamic_cast<CRewardableObject*>(object);
+
+	uint8_t bonusTypeRaw = reader->readUInt8();
+	auto bonusType = static_cast<ScholarBonusType>(bonusTypeRaw);
+	auto bonusID = reader->readUInt8();
+
+	switch (bonusType)
+	{
+		case ScholarBonusType::PRIM_SKILL:
+		{
+			JsonNode variable;
+			JsonNode dice;
+			variable.String() = NPrimarySkill::names[bonusID];
+			variable.setMeta(ModScope::scopeGame());
+			dice.Integer() = 80;
+			rewardable->configuration.presetVariable("primarySkill", "gainedStat", variable);
+			rewardable->configuration.presetVariable("dice", "0", dice);
+			break;
+		}
+		case ScholarBonusType::SECONDARY_SKILL:
+		{
+			JsonNode variable;
+			JsonNode dice;
+			variable.String() = VLC->skills()->getByIndex(bonusID)->getJsonKey();
+			variable.setMeta(ModScope::scopeGame());
+			dice.Integer() = 50;
+			rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable);
+			rewardable->configuration.presetVariable("dice", "0", dice);
+			break;
+		}
+		case ScholarBonusType::SPELL:
+		{
+			JsonNode variable;
+			JsonNode dice;
+			variable.String() = VLC->spells()->getByIndex(bonusID)->getJsonKey();
+			variable.setMeta(ModScope::scopeGame());
+			dice.Integer() = 20;
+			rewardable->configuration.presetVariable("spell", "gainedSpell", variable);
+			rewardable->configuration.presetVariable("dice", "0", dice);
+			break;
+		}
+		case ScholarBonusType::RANDOM:
+			break;// No-op
+		default:
+			logGlobal->warn("Map '%s': Invalid Scholar settings! Ignoring...", mapName);
+	}
+
 	reader->skipZero(6);
 	return object;
 }
@@ -1306,10 +1378,22 @@ CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, s
 	return object;
 }
 
-CGObjectInstance * CMapLoaderH3M::readShrine()
+CGObjectInstance * CMapLoaderH3M::readShrine(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate)
 {
-	auto * object = new CGShrine();
-	object->spell = reader->readSpell32();
+	auto * object = readGeneric(position, objectTemplate);
+	auto * rewardable = dynamic_cast<CRewardableObject*>(object);
+
+	assert(rewardable);
+
+	SpellID spell = reader->readSpell32();
+
+	if(spell != SpellID::NONE)
+	{
+		JsonNode variable;
+		variable.String() = VLC->spells()->getById(spell)->getJsonKey();
+		variable.setMeta(ModScope::scopeGame()); // list may include spells from all mods
+		rewardable->configuration.presetVariable("spell", "gainedSpell", variable);
+	}
 	return object;
 }
 
@@ -1458,9 +1542,9 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr<const ObjectTemplat
 			return readSeerHut(mapPosition, objectInstanceID);
 
 		case Obj::WITCH_HUT:
-			return readWitchHut();
+			return readWitchHut(mapPosition, objectTemplate);
 		case Obj::SCHOLAR:
-			return readScholar();
+			return readScholar(mapPosition, objectTemplate);
 
 		case Obj::GARRISON:
 		case Obj::GARRISON2:
@@ -1495,7 +1579,7 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr<const ObjectTemplat
 		case Obj::SHRINE_OF_MAGIC_INCANTATION:
 		case Obj::SHRINE_OF_MAGIC_GESTURE:
 		case Obj::SHRINE_OF_MAGIC_THOUGHT:
-			return readShrine();
+			return readShrine(mapPosition, objectTemplate);
 
 		case Obj::PANDORAS_BOX:
 			return readPandora(mapPosition, objectInstanceID);
@@ -1717,7 +1801,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec
 	{
 		if(!object->secSkills.empty())
 		{
-			if(object->secSkills[0].first != SecondarySkill::DEFAULT)
+			if(object->secSkills[0].first != SecondarySkill::NONE)
 				logGlobal->debug("Map '%s': Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", mapName, object->getNameTextID(), object->subID);
 			object->secSkills.clear();
 		}

+ 3 - 3
lib/mapping/MapFormatH3M.h

@@ -165,8 +165,8 @@ private:
 	CGObjectInstance * readSeerHut(const int3 & initialPos, const ObjectInstanceID & idToBeGiven);
 	CGObjectInstance * readTown(const int3 & position, std::shared_ptr<const ObjectTemplate> objTempl);
 	CGObjectInstance * readSign(const int3 & position);
-	CGObjectInstance * readWitchHut();
-	CGObjectInstance * readScholar();
+	CGObjectInstance * readWitchHut(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate);
+	CGObjectInstance * readScholar(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate);
 	CGObjectInstance * readGarrison(const int3 & mapPosition);
 	CGObjectInstance * readArtifact(const int3 & position, std::shared_ptr<const ObjectTemplate> objTempl);
 	CGObjectInstance * readResource(const int3 & position, std::shared_ptr<const ObjectTemplate> objTempl);
@@ -174,7 +174,7 @@ private:
 	CGObjectInstance * readPandora(const int3 & position, const ObjectInstanceID & idToBeGiven);
 	CGObjectInstance * readDwelling(const int3 & position);
 	CGObjectInstance * readDwellingRandom(const int3 & position, std::shared_ptr<const ObjectTemplate> objTempl);
-	CGObjectInstance * readShrine();
+	CGObjectInstance * readShrine(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate);
 	CGObjectInstance * readHeroPlaceholder(const int3 & position);
 	CGObjectInstance * readGrail(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate);
 	CGObjectInstance * readPyramid(const int3 & position, std::shared_ptr<const ObjectTemplate> objTempl);

+ 1 - 1
lib/pathfinder/CPathfinder.cpp

@@ -368,7 +368,7 @@ void CPathfinderHelper::initializePatrol()
 		if(hero->patrol.patrolRadius)
 		{
 			state = PATROL_RADIUS;
-			gs->getTilesInRange(patrolTiles, hero->patrol.initialPos, hero->patrol.patrolRadius, std::optional<PlayerColor>(), 0, int3::DIST_MANHATTAN);
+			gs->getTilesInRange(patrolTiles, hero->patrol.initialPos, hero->patrol.patrolRadius, ETileVisibility::REVEALED, std::optional<PlayerColor>(), int3::DIST_MANHATTAN);
 		}
 		else
 			state = PATROL_LOCKED;

+ 0 - 12
lib/registerTypes/RegisterTypes.h

@@ -23,7 +23,6 @@
 #include "../mapObjectConstructors/DwellingInstanceConstructor.h"
 #include "../mapObjectConstructors/HillFortInstanceConstructor.h"
 #include "../mapObjectConstructors/ShipyardInstanceConstructor.h"
-#include "../mapObjectConstructors/ShrineInstanceConstructor.h"
 #include "../mapObjects/MapObjects.h"
 #include "../mapObjects/CGCreature.h"
 #include "../mapObjects/CGTownBuilding.h"
@@ -55,8 +54,6 @@ void registerTypesMapObjects1(Serializer &s)
 			s.template registerType<CGMonolith, CGSubterraneanGate>();
 			s.template registerType<CGMonolith, CGWhirlpool>();
 	s.template registerType<CGObjectInstance, CGSignBottle>();
-	s.template registerType<CGObjectInstance, CGScholar>();
-	s.template registerType<CGObjectInstance, CGObservatory>();
 	s.template registerType<CGObjectInstance, CGKeys>();
 		s.template registerType<CGKeys, CGKeymasterTent>();
 		s.template registerType<CGKeys, CGBorderGuard>(); s.template registerType<IQuestObject, CGBorderGuard>();
@@ -103,7 +100,6 @@ void registerTypesMapObjectTypes(Serializer &s)
 	s.template registerType<AObjectTypeHandler, BoatInstanceConstructor>();
 	s.template registerType<AObjectTypeHandler, MarketInstanceConstructor>();
 	s.template registerType<AObjectTypeHandler, CObstacleConstructor>();
-	s.template registerType<AObjectTypeHandler, ShrineInstanceConstructor>();
 	s.template registerType<AObjectTypeHandler, ShipyardInstanceConstructor>();
 	s.template registerType<AObjectTypeHandler, HillFortInstanceConstructor>();
 	s.template registerType<AObjectTypeHandler, CreatureInstanceConstructor>();
@@ -112,7 +108,6 @@ void registerTypesMapObjectTypes(Serializer &s)
 #define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType<AObjectTypeHandler, CDefaultObjectTypeHandler<TYPENAME> >()
 
 	REGISTER_GENERIC_HANDLER(CGObjectInstance);
-	REGISTER_GENERIC_HANDLER(CCartographer);
 	REGISTER_GENERIC_HANDLER(CGArtifact);
 	REGISTER_GENERIC_HANDLER(CGBlackMarket);
 	REGISTER_GENERIC_HANDLER(CGBoat);
@@ -132,14 +127,11 @@ void registerTypesMapObjectTypes(Serializer &s)
 	REGISTER_GENERIC_HANDLER(CGMarket);
 	REGISTER_GENERIC_HANDLER(CGMine);
 	REGISTER_GENERIC_HANDLER(CGObelisk);
-	REGISTER_GENERIC_HANDLER(CGObservatory);
 	REGISTER_GENERIC_HANDLER(CGPandoraBox);
 	REGISTER_GENERIC_HANDLER(CGQuestGuard);
 	REGISTER_GENERIC_HANDLER(CGResource);
-	REGISTER_GENERIC_HANDLER(CGScholar);
 	REGISTER_GENERIC_HANDLER(CGSeerHut);
 	REGISTER_GENERIC_HANDLER(CGShipyard);
-	REGISTER_GENERIC_HANDLER(CGShrine);
 	REGISTER_GENERIC_HANDLER(CGSignBottle);
 	REGISTER_GENERIC_HANDLER(CGSirens);
 	REGISTER_GENERIC_HANDLER(CGMonolith);
@@ -147,7 +139,6 @@ void registerTypesMapObjectTypes(Serializer &s)
 	REGISTER_GENERIC_HANDLER(CGWhirlpool);
 	REGISTER_GENERIC_HANDLER(CGTownInstance);
 	REGISTER_GENERIC_HANDLER(CGUniversity);
-	REGISTER_GENERIC_HANDLER(CGWitchHut);
 	REGISTER_GENERIC_HANDLER(HillFort);
 
 #undef REGISTER_GENERIC_HANDLER
@@ -177,9 +168,6 @@ void registerTypesMapObjects2(Serializer &s)
 	s.template registerType<CGObjectInstance, CRewardableObject>();
 
 	s.template registerType<CGObjectInstance, CTeamVisited>();
-		s.template registerType<CTeamVisited, CGWitchHut>();
-		s.template registerType<CTeamVisited, CGShrine>();
-		s.template registerType<CTeamVisited, CCartographer>();
 		s.template registerType<CTeamVisited, CGObelisk>();
 
 	//s.template registerType<CQuest>();

+ 54 - 0
lib/rewardable/Configuration.cpp

@@ -24,6 +24,38 @@ ui16 Rewardable::Configuration::getResetDuration() const
 	return resetParameters.period;
 }
 
+std::optional<int> Rewardable::Configuration::getVariable(const std::string & category, const std::string & name) const
+{
+	std::string variableID = category + '@' + name;
+
+	if (variables.values.count(variableID))
+		return variables.values.at(variableID);
+
+	return std::nullopt;
+}
+
+JsonNode Rewardable::Configuration::getPresetVariable(const std::string & category, const std::string & name) const
+{
+	std::string variableID = category + '@' + name;
+
+	if (variables.preset.count(variableID))
+		return variables.preset.at(variableID);
+	else
+		return JsonNode();
+}
+
+void Rewardable::Configuration::presetVariable(const std::string & category, const std::string & name, const JsonNode & value)
+{
+	std::string variableID = category + '@' + name;
+	variables.preset[variableID] = value;
+}
+
+void Rewardable::Configuration::initVariable(const std::string & category, const std::string & name, int value)
+{
+	std::string variableID = category + '@' + name;
+	variables.values[variableID] = value;
+}
+
 void Rewardable::ResetInfo::serializeJson(JsonSerializeFormat & handler)
 {
 	handler.serializeInt("period", period);
@@ -39,6 +71,27 @@ void Rewardable::VisitInfo::serializeJson(JsonSerializeFormat & handler)
 	handler.serializeInt("visitType", visitType);
 }
 
+void Rewardable::Variables::serializeJson(JsonSerializeFormat & handler)
+{
+	if (handler.saving)
+	{
+		JsonNode presetNode;
+		for (auto const & entry : preset)
+			presetNode[entry.first] = entry.second;
+
+		handler.serializeRaw("preset", presetNode, {});
+	}
+	else
+	{
+		preset.clear();
+		JsonNode presetNode;
+		handler.serializeRaw("preset", presetNode, {});
+
+		for (auto const & entry : presetNode.Struct())
+			preset[entry.first] = entry.second;
+	}
+}
+
 void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler)
 {
 	handler.serializeStruct("onSelect", onSelect);
@@ -47,6 +100,7 @@ void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler)
 	handler.serializeEnum("visitMode", visitMode, std::vector<std::string>{VisitModeString.begin(), VisitModeString.end()});
 	handler.serializeStruct("resetParameters", resetParameters);
 	handler.serializeBool("canRefuse", canRefuse);
+	handler.serializeBool("showScoutedPreview", showScoutedPreview);
 	handler.serializeInt("infoWindowType", infoWindowType);
 }
 

+ 59 - 7
lib/rewardable/Configuration.h

@@ -26,6 +26,7 @@ enum EVisitMode
 	VISIT_ONCE,      // only once, first to visit get all the rewards
 	VISIT_HERO,      // every hero can visit object once
 	VISIT_BONUS,     // can be visited by any hero that don't have bonus from this object
+	VISIT_LIMITER,   // can be visited by heroes that don't fulfill provided limiter
 	VISIT_PLAYER     // every player can visit object once
 };
 
@@ -46,7 +47,7 @@ enum class EEventType
 };
 
 const std::array<std::string, 3> SelectModeString{"selectFirst", "selectPlayer", "selectRandom"};
-const std::array<std::string, 5> VisitModeString{"unlimited", "once", "hero", "bonus", "player"};
+const std::array<std::string, 6> VisitModeString{"unlimited", "once", "hero", "bonus", "limiter", "player"};
 
 struct DLL_LINKAGE ResetInfo
 {
@@ -62,7 +63,6 @@ struct DLL_LINKAGE ResetInfo
 	/// if true - reset list of visitors (heroes & players) on reset
 	bool visitors;
 
-
 	/// if true - re-randomize rewards on a new week
 	bool rewards;
 	
@@ -84,9 +84,13 @@ struct DLL_LINKAGE VisitInfo
 	/// Message that will be displayed on granting of this reward, if not empty
 	MetaString message;
 
+	/// Object description that will be shown on right-click, after object name
+	/// Used only after player have "scouted" object and knows internal state of an object
+	MetaString description;
+
 	/// Event to which this reward is assigned
 	EEventType visitType;
-	
+
 	void serializeJson(JsonSerializeFormat & handler);
 
 	template <typename Handler> void serialize(Handler &h, const int version)
@@ -94,16 +98,44 @@ struct DLL_LINKAGE VisitInfo
 		h & limiter;
 		h & reward;
 		h & message;
+		h & description;
 		h & visitType;
 	}
 };
 
+struct DLL_LINKAGE Variables
+{
+	/// List of variables used by this object in their current values
+	std::map<std::string, int> values;
+
+	/// List of per-instance preconfigured variables, e.g. from map
+	std::map<std::string, JsonNode> preset;
+
+	void serializeJson(JsonSerializeFormat & handler);
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & values;
+		h & preset;
+	}
+};
+
 /// Base class that can handle granting rewards to visiting heroes.
 struct DLL_LINKAGE Configuration
 {
 	/// Message that will be shown if player needs to select one of multiple rewards
 	MetaString onSelect;
 
+	/// Object description that will be shown on right-click, after object name
+	/// Used only if player is not aware of object internal state, e.g. have never visited it
+	MetaString description;
+
+	/// Text that will be shown if hero has not visited this object
+	MetaString notVisitedTooltip;
+
+	/// Text that will be shown after hero has visited this object
+	MetaString visitedTooltip;
+
 	/// Rewards that can be applied by an object
 	std::vector<Rewardable::VisitInfo> info;
 
@@ -116,25 +148,45 @@ struct DLL_LINKAGE Configuration
 	/// how and when should the object be reset
 	Rewardable::ResetInfo resetParameters;
 
+	/// List of variables shoread between all limiters and rewards
+	Rewardable::Variables variables;
+
+	/// Limiter that will be used to determine that object is visited. Only if visit mode is set to "limiter"
+	Rewardable::Limiter visitLimiter;
+
 	/// if true - player can refuse visiting an object (e.g. Tomb)
 	bool canRefuse = false;
 
+	/// if true - right-clicking object will show preview of object rewards
+	bool showScoutedPreview = false;
+
 	/// if true - object info will shown in infobox (like resource pickup)
 	EInfoWindowMode infoWindowType = EInfoWindowMode::AUTO;
 	
 	EVisitMode getVisitMode() const;
 	ui16 getResetDuration() const;
+
+	std::optional<int> getVariable(const std::string & category, const std::string & name) const;
+	JsonNode getPresetVariable(const std::string & category, const std::string & name) const;
+	void presetVariable(const std::string & category, const std::string & name, const JsonNode & value);
+	void initVariable(const std::string & category, const std::string & name, int value);
 	
 	void serializeJson(JsonSerializeFormat & handler);
 	
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
-		h & info;
-		h & canRefuse;
-		h & resetParameters;
 		h & onSelect;
-		h & visitMode;
+		h & description;
+		h & notVisitedTooltip;
+		h & visitedTooltip;
+		h & info;
 		h & selectMode;
+		h & visitMode;
+		h & resetParameters;
+		h & variables;
+		h & visitLimiter;
+		h & canRefuse;
+		h & showScoutedPreview;
 		h & infoWindowType;
 	}
 };

+ 167 - 56
lib/rewardable/Info.cpp

@@ -24,19 +24,36 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 namespace {
-	MetaString loadMessage(const JsonNode & value, const TextIdentifier & textIdentifier )
+	MetaString loadMessage(const JsonNode & value, const TextIdentifier & textIdentifier, EMetaText textSource = EMetaText::ADVOB_TXT )
 	{
 		MetaString ret;
+
+		if (value.isVector())
+		{
+			for(const auto & entry : value.Vector())
+			{
+				if (entry.isNumber())
+					ret.appendLocalString(textSource, static_cast<ui32>(entry.Float()));
+				if (entry.isString())
+					ret.appendRawString(entry.String());
+			}
+			return ret;
+		}
+
 		if (value.isNumber())
 		{
-			ret.appendLocalString(EMetaText::ADVOB_TXT, static_cast<ui32>(value.Float()));
+			ret.appendLocalString(textSource, static_cast<ui32>(value.Float()));
 			return ret;
 		}
 
 		if (value.String().empty())
 			return ret;
 
-		ret.appendTextID(textIdentifier.get());
+		if (value.String()[0] == '@')
+			ret.appendTextID(value.String().substr(1));
+		else
+			ret.appendTextID(textIdentifier.get());
+
 		return ret;
 	}
 
@@ -56,7 +73,7 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o
 	objectTextID = objectName;
 
 	auto loadString = [&](const JsonNode & entry, const TextIdentifier & textID){
-		if (entry.isString() && !entry.String().empty())
+		if (entry.isString() && !entry.String().empty() && entry.String()[0] != '@')
 			VLC->generaltexth->registerString(entry.meta, textID, entry.String());
 	};
 
@@ -81,6 +98,9 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o
 	}
 
 	loadString(parameters["onSelectMessage"], TextIdentifier(objectName, "onSelect"));
+	loadString(parameters["description"], TextIdentifier(objectName, "description"));
+	loadString(parameters["notVisitedTooltip"], TextIdentifier(objectName, "notVisitedText"));
+	loadString(parameters["visitedTooltip"], TextIdentifier(objectName, "visitedTooltip"));
 	loadString(parameters["onVisitedMessage"], TextIdentifier(objectName, "onVisited"));
 	loadString(parameters["onEmptyMessage"], TextIdentifier(objectName, "onEmpty"));
 }
@@ -102,28 +122,27 @@ Rewardable::LimitersList Rewardable::Info::configureSublimiters(Rewardable::Conf
 
 void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Limiter & limiter, const JsonNode & source) const
 {
-	std::vector<SpellID> spells;
-	IObjectInterface::cb->getAllowedSpells(spells);
-
+	auto const & variables = object.variables.values;
 
-	limiter.dayOfWeek = JsonRandom::loadValue(source["dayOfWeek"], rng);
-	limiter.daysPassed = JsonRandom::loadValue(source["daysPassed"], rng);
-	limiter.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng);
-	limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng)
-					 + JsonRandom::loadValue(source["minLevel"], rng); // VCMI 1.1 compatibilty
+	limiter.dayOfWeek = JsonRandom::loadValue(source["dayOfWeek"], rng, variables);
+	limiter.daysPassed = JsonRandom::loadValue(source["daysPassed"], rng, variables);
+	limiter.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables);
+	limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables);
+	limiter.canLearnSkills = source["canLearnSkills"].Bool();
 
-	limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng);
-	limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng);
+	limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables);
+	limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng, variables);
 
-	limiter.resources = JsonRandom::loadResources(source["resources"], rng);
+	limiter.resources = JsonRandom::loadResources(source["resources"], rng, variables);
 
-	limiter.primary = JsonRandom::loadPrimary(source["primary"], rng);
-	limiter.secondary = JsonRandom::loadSecondary(source["secondary"], rng);
-	limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng);
-	limiter.spells  = JsonRandom::loadSpells(source["spells"], rng, spells);
-	limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng);
+	limiter.primary = JsonRandom::loadPrimaries(source["primary"], rng, variables);
+	limiter.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables);
+	limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables);
+	limiter.spells  = JsonRandom::loadSpells(source["spells"], rng, variables);
+	limiter.canLearnSpells  = JsonRandom::loadSpells(source["canLearnSpells"], rng, variables);
+	limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables);
 	
-	limiter.players = JsonRandom::loadColors(source["colors"], rng);
+	limiter.players = JsonRandom::loadColors(source["colors"], rng, variables);
 	limiter.heroes = JsonRandom::loadHeroes(source["heroes"], rng);
 	limiter.heroClasses = JsonRandom::loadHeroClasses(source["heroClasses"], rng);
 
@@ -134,39 +153,49 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan
 
 void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Reward & reward, const JsonNode & source) const
 {
-	reward.resources = JsonRandom::loadResources(source["resources"], rng);
+	auto const & variables = object.variables.values;
 
-	reward.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng)
-						  + JsonRandom::loadValue(source["gainedExp"], rng); // VCMI 1.1 compatibilty
+	reward.resources = JsonRandom::loadResources(source["resources"], rng, variables);
 
-	reward.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng)
-						+ JsonRandom::loadValue(source["gainedLevels"], rng); // VCMI 1.1 compatibilty
+	reward.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables);
+	reward.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables);
 
-	reward.manaDiff = JsonRandom::loadValue(source["manaPoints"], rng);
-	reward.manaOverflowFactor = JsonRandom::loadValue(source["manaOverflowFactor"], rng);
-	reward.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, -1);
+	reward.manaDiff = JsonRandom::loadValue(source["manaPoints"], rng, variables);
+	reward.manaOverflowFactor = JsonRandom::loadValue(source["manaOverflowFactor"], rng, variables);
+	reward.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables, -1);
 
-	reward.movePoints = JsonRandom::loadValue(source["movePoints"], rng);
-	reward.movePercentage = JsonRandom::loadValue(source["movePercentage"], rng, -1);
+	reward.movePoints = JsonRandom::loadValue(source["movePoints"], rng, variables);
+	reward.movePercentage = JsonRandom::loadValue(source["movePercentage"], rng, variables, -1);
 
 	reward.removeObject = source["removeObject"].Bool();
 	reward.bonuses = JsonRandom::loadBonuses(source["bonuses"]);
 
-	reward.primary = JsonRandom::loadPrimary(source["primary"], rng);
-	reward.secondary = JsonRandom::loadSecondary(source["secondary"], rng);
-
-	std::vector<SpellID> spells;
-	IObjectInterface::cb->getAllowedSpells(spells);
+	reward.primary = JsonRandom::loadPrimaries(source["primary"], rng, variables);
+	reward.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables);
 
-	reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng);
-	reward.spells = JsonRandom::loadSpells(source["spells"], rng, spells);
-	reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng);
+	reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables);
+	reward.spells = JsonRandom::loadSpells(source["spells"], rng, variables);
+	reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables);
 	if(!source["spellCast"].isNull() && source["spellCast"].isStruct())
 	{
-		reward.spellCast.first = JsonRandom::loadSpell(source["spellCast"]["spell"], rng);
+		reward.spellCast.first = JsonRandom::loadSpell(source["spellCast"]["spell"], rng, variables);
 		reward.spellCast.second = source["spellCast"]["schoolLevel"].Integer();
 	}
 
+	if (!source["revealTiles"].isNull())
+	{
+		auto const & entry = source["revealTiles"];
+
+		reward.revealTiles = RewardRevealTiles();
+		reward.revealTiles->radius = JsonRandom::loadValue(entry["radius"], rng, variables);
+		reward.revealTiles->hide = entry["hide"].Bool();
+
+		reward.revealTiles->scoreSurface = JsonRandom::loadValue(entry["surface"], rng, variables);
+		reward.revealTiles->scoreSubterra = JsonRandom::loadValue(entry["subterra"], rng, variables);
+		reward.revealTiles->scoreWater = JsonRandom::loadValue(entry["water"], rng, variables);
+		reward.revealTiles->scoreRock = JsonRandom::loadValue(entry["rock"], rng, variables);
+	}
+
 	for ( auto node : source["changeCreatures"].Struct() )
 	{
 		CreatureID from(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.first).value());
@@ -185,11 +214,66 @@ void Rewardable::Info::configureResetInfo(Rewardable::Configuration & object, CR
 	resetParameters.rewards  = source["rewards"].Bool();
 }
 
+void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const
+{
+	for(const auto & category : source.Struct())
+	{
+		for(const auto & entry : category.second.Struct())
+		{
+			JsonNode preset = object.getPresetVariable(category.first, entry.first);
+			const JsonNode & input = preset.isNull() ? entry.second : preset;
+			int32_t value = -1;
+
+			if (category.first == "number")
+				value = JsonRandom::loadValue(input, rng, object.variables.values);
+
+			if (category.first == "artifact")
+				value = JsonRandom::loadArtifact(input, rng, object.variables.values).getNum();
+
+			if (category.first == "spell")
+				value = JsonRandom::loadSpell(input, rng, object.variables.values).getNum();
+
+			if (category.first == "primarySkill")
+				value = static_cast<int>(JsonRandom::loadPrimary(input, rng, object.variables.values));
+
+			if (category.first == "secondarySkill")
+				value = JsonRandom::loadSecondary(input, rng, object.variables.values).getNum();
+
+			object.initVariable(category.first, entry.first, value);
+		}
+	}
+}
+
+void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables) const
+{
+	for (const auto & variable : variables.values )
+	{
+		if( boost::algorithm::starts_with(variable.first, "spell"))
+			target.replaceLocalString(EMetaText::SPELL_NAME, variable.second);
+
+		if( boost::algorithm::starts_with(variable.first, "secondarySkill"))
+			target.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second);
+	}
+}
+
+void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const
+{
+	for (const auto & artifact : info.reward.artifacts )
+		target.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum());
+
+	for (const auto & artifact : info.reward.spells )
+		target.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum());
+
+	for (const auto & secondary : info.reward.secondary )
+		target.replaceLocalString(EMetaText::SEC_SKILL_NAME, secondary.first.getNum());
+
+	replaceTextPlaceholders(target, variables);
+}
+
 void Rewardable::Info::configureRewards(
 		Rewardable::Configuration & object,
 		CRandomGenerator & rng, const
 		JsonNode & source,
-		std::map<si32, si32> & thrownDice,
 		Rewardable::EEventType event,
 		const std::string & modeName) const
 {
@@ -200,21 +284,32 @@ void Rewardable::Info::configureRewards(
 		if (!reward["appearChance"].isNull())
 		{
 			JsonNode chance = reward["appearChance"];
-			si32 diceID = static_cast<si32>(chance["dice"].Float());
+			std::string diceID = std::to_string(chance["dice"].Integer());
+
+			auto diceValue = object.getVariable("dice", diceID);
+
+			if (!diceValue.has_value())
+			{
+				const JsonNode & preset = object.getPresetVariable("dice", diceID);
+				if (preset.isNull())
+					object.initVariable("dice", diceID, rng.getIntRange(0, 99)());
+				else
+					object.initVariable("dice", diceID, preset.Integer());
 
-			if (thrownDice.count(diceID) == 0)
-				thrownDice[diceID] = rng.getIntRange(0, 99)();
+				diceValue = object.getVariable("dice", diceID);
+			}
+			assert(diceValue.has_value());
 
 			if (!chance["min"].isNull())
 			{
 				int min = static_cast<int>(chance["min"].Float());
-				if (min > thrownDice[diceID])
+				if (min > *diceValue)
 					continue;
 			}
 			if (!chance["max"].isNull())
 			{
 				int max = static_cast<int>(chance["max"].Float());
-				if (max <= thrownDice[diceID])
+				if (max <= *diceValue)
 					continue;
 			}
 		}
@@ -225,12 +320,10 @@ void Rewardable::Info::configureRewards(
 
 		info.visitType = event;
 		info.message = loadMessage(reward["message"], TextIdentifier(objectTextID, modeName, i));
+		info.description = loadMessage(reward["description"], TextIdentifier(objectTextID, "description", modeName, i), EMetaText::GENERAL_TXT);
 
-		for (const auto & artifact : info.reward.artifacts )
-			info.message.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum());
-
-		for (const auto & artifact : info.reward.spells )
-			info.message.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum());
+		replaceTextPlaceholders(info.message, object.variables, info);
+		replaceTextPlaceholders(info.description, object.variables, info);
 
 		object.info.push_back(info);
 	}
@@ -240,19 +333,30 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand
 {
 	object.info.clear();
 
-	std::map<si32, si32> thrownDice;
+	configureVariables(object, rng, parameters["variables"]);
 
-	configureRewards(object, rng, parameters["rewards"], thrownDice, Rewardable::EEventType::EVENT_FIRST_VISIT, "rewards");
-	configureRewards(object, rng, parameters["onVisited"], thrownDice, Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited");
-	configureRewards(object, rng, parameters["onEmpty"], thrownDice, Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty");
+	configureRewards(object, rng, parameters["rewards"], Rewardable::EEventType::EVENT_FIRST_VISIT, "rewards");
+	configureRewards(object, rng, parameters["onVisited"], Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited");
+	configureRewards(object, rng, parameters["onEmpty"], Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty");
 
-	object.onSelect   = loadMessage(parameters["onSelectMessage"], TextIdentifier(objectTextID, "onSelect"));
+	object.onSelect = loadMessage(parameters["onSelectMessage"], TextIdentifier(objectTextID, "onSelect"));
+	object.description = loadMessage(parameters["description"], TextIdentifier(objectTextID, "description"));
+	object.notVisitedTooltip = loadMessage(parameters["notVisitedTooltip"], TextIdentifier(objectTextID, "notVisitedTooltip"), EMetaText::GENERAL_TXT);
+	object.visitedTooltip = loadMessage(parameters["visitedTooltip"], TextIdentifier(objectTextID, "visitedTooltip"), EMetaText::GENERAL_TXT);
+
+	if (object.notVisitedTooltip.empty())
+		object.notVisitedTooltip.appendTextID("core.genrltxt.353");
+
+	if (object.visitedTooltip.empty())
+		object.visitedTooltip.appendTextID("core.genrltxt.352");
 
 	if (!parameters["onVisitedMessage"].isNull())
 	{
 		Rewardable::VisitInfo onVisited;
 		onVisited.visitType = Rewardable::EEventType::EVENT_ALREADY_VISITED;
 		onVisited.message = loadMessage(parameters["onVisitedMessage"], TextIdentifier(objectTextID, "onVisited"));
+		replaceTextPlaceholders(onVisited.message, object.variables);
+
 		object.info.push_back(onVisited);
 	}
 
@@ -261,12 +365,15 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand
 		Rewardable::VisitInfo onEmpty;
 		onEmpty.visitType = Rewardable::EEventType::EVENT_NOT_AVAILABLE;
 		onEmpty.message = loadMessage(parameters["onEmptyMessage"], TextIdentifier(objectTextID, "onEmpty"));
+		replaceTextPlaceholders(onEmpty.message, object.variables);
+
 		object.info.push_back(onEmpty);
 	}
 
 	configureResetInfo(object, rng, object.resetParameters, parameters["resetParameters"]);
 
 	object.canRefuse = parameters["canRefuse"].Bool();
+	object.showScoutedPreview = parameters["showScoutedPreview"].Bool();
 
 	if(parameters["showInInfobox"].isNull())
 		object.infoWindowType = EInfoWindowMode::AUTO;
@@ -292,6 +399,10 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand
 			break;
 		}
 	}
+
+	if (object.visitMode == Rewardable::VISIT_LIMITER)
+		configureLimiter(object, rng, object.visitLimiter, parameters["visitLimiter"]);
+
 }
 
 bool Rewardable::Info::givesResources() const

+ 8 - 1
lib/rewardable/Info.h

@@ -16,6 +16,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 class CRandomGenerator;
+class MetaString;
 
 namespace Rewardable
 {
@@ -24,6 +25,8 @@ struct Limiter;
 using LimitersList = std::vector<std::shared_ptr<Rewardable::Limiter>>;
 struct Reward;
 struct Configuration;
+struct Variables;
+struct VisitInfo;
 struct ResetInfo;
 enum class EEventType;
 
@@ -32,7 +35,11 @@ class DLL_LINKAGE Info : public IObjectInfo
 	JsonNode parameters;
 	std::string objectTextID;
 
-	void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, std::map<si32, si32> & thrownDice, Rewardable::EEventType mode, const std::string & textPrefix) const;
+	void replaceTextPlaceholders(MetaString & target, const Variables & variables) const;
+	void replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const;
+
+	void configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const;
+	void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, Rewardable::EEventType mode, const std::string & textPrefix) const;
 
 	void configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Limiter & limiter, const JsonNode & source) const;
 	Rewardable::LimitersList configureSublimiters(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const;

+ 55 - 0
lib/rewardable/Interface.cpp

@@ -12,8 +12,11 @@
 #include "Interface.h"
 
 #include "../CHeroHandler.h"
+#include "../TerrainHandler.h"
+#include "../CPlayerState.h"
 #include "../CSoundBase.h"
 #include "../NetPacks.h"
+#include "../gameState/CGameState.h"
 #include "../spells/CSpellHandler.h"
 #include "../spells/ISpellMechanics.h"
 #include "../mapObjects/MiscObjects.h"
@@ -46,6 +49,58 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R
 
 	cb->giveResources(hero->tempOwner, info.reward.resources);
 
+	if (info.reward.revealTiles)
+	{
+		const auto & props = *info.reward.revealTiles;
+
+		const auto functor = [&props](const TerrainTile * tile)
+		{
+			int score = 0;
+			if (tile->terType->isSurface())
+				score += props.scoreSurface;
+
+			if (tile->terType->isUnderground())
+				score += props.scoreSubterra;
+
+			if (tile->terType->isWater())
+				score += props.scoreWater;
+
+			if (tile->terType->isRock())
+				score += props.scoreRock;
+
+			return score > 0;
+		};
+
+		std::unordered_set<int3> tiles;
+		if (props.radius > 0)
+		{
+			cb->getTilesInRange(tiles, hero->getSightCenter(), props.radius, ETileVisibility::HIDDEN, hero->getOwner());
+			if (props.hide)
+				cb->getTilesInRange(tiles, hero->getSightCenter(), props.radius, ETileVisibility::REVEALED, hero->getOwner());
+
+			vstd::erase_if(tiles, [&](const int3 & coord){
+				return !functor(cb->getTile(coord));
+			});
+		}
+		else
+		{
+			cb->getAllTiles(tiles, hero->tempOwner, -1, functor);
+		}
+
+		if (props.hide)
+		{
+			for (auto & player : cb->gameState()->players)
+			{
+				if (cb->getPlayerStatus(player.first) == EPlayerStatus::INGAME && cb->getPlayerRelations(player.first, hero->getOwner()) == PlayerRelations::ENEMIES)
+					cb->changeFogOfWar(tiles, player.first, ETileVisibility::HIDDEN);
+			}
+		}
+		else
+		{
+			cb->changeFogOfWar(tiles, hero->getOwner(), ETileVisibility::REVEALED);
+		}
+	}
+
 	for(const auto & entry : info.reward.secondary)
 	{
 		int current = hero->getSecSkillLevel(entry.first);

+ 0 - 2
lib/rewardable/Interface.h

@@ -10,8 +10,6 @@
 
 #pragma once
 
-#include "../CCreatureSet.h"
-#include "../ResourceSet.h"
 #include "../spells/ExternalCaster.h"
 #include "Configuration.h"
 

+ 9 - 0
lib/rewardable/Limiter.cpp

@@ -101,6 +101,9 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const
 	if(manaPoints > hero->mana)
 		return false;
 
+	if (canLearnSkills && !hero->canLearnSkill())
+		return false;
+
 	if(manaPercentage > 100 * hero->mana / hero->manaLimit())
 		return false;
 
@@ -122,6 +125,12 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const
 			return false;
 	}
 
+	for(const auto & spell : canLearnSpells)
+	{
+		if (!hero->canLearnSpell(spell.toSpell(VLC->spells()), true))
+			return false;
+	}
+
 	{
 		std::unordered_map<ArtifactID, unsigned int, ArtifactID::hash> artifactsRequirements; // artifact ID -> required count
 		for(const auto & art : artifacts)

+ 9 - 0
lib/rewardable/Limiter.h

@@ -44,6 +44,9 @@ struct DLL_LINKAGE Limiter final
 	/// percentage of mana points that hero needs to have
 	si32 manaPercentage;
 
+	/// Number of free secondary slots that hero needs to have
+	bool canLearnSkills;
+
 	/// resources player needs to have in order to trigger reward
 	TResources resources;
 
@@ -58,6 +61,9 @@ struct DLL_LINKAGE Limiter final
 	/// Spells that hero must have in the spellbook
 	std::vector<SpellID> spells;
 
+	/// Spells that hero must be able to learn
+	std::vector<SpellID> canLearnSpells;
+
 	/// creatures that hero needs to have
 	std::vector<CStackBasicDescriptor> creatures;
 	
@@ -94,10 +100,13 @@ struct DLL_LINKAGE Limiter final
 		h & heroLevel;
 		h & manaPoints;
 		h & manaPercentage;
+		h & canLearnSkills;
 		h & resources;
 		h & primary;
 		h & secondary;
 		h & artifacts;
+		h & spells;
+		h & canLearnSpells;
 		h & creatures;
 		h & heroes;
 		h & heroClasses;

+ 14 - 6
lib/rewardable/Reward.cpp

@@ -18,6 +18,16 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+void Rewardable::RewardRevealTiles::serializeJson(JsonSerializeFormat & handler)
+{
+	handler.serializeBool("hide", hide);
+	handler.serializeInt("scoreSurface", scoreSurface);
+	handler.serializeInt("scoreSubterra", scoreSubterra);
+	handler.serializeInt("scoreWater", scoreWater);
+	handler.serializeInt("scoreRock", scoreRock);
+	handler.serializeInt("radius", radius);
+}
+
 Rewardable::Reward::Reward()
 	: heroExperience(0)
 	, heroLevel(0)
@@ -56,8 +66,7 @@ Component Rewardable::Reward::getDisplayedComponent(const CGHeroInstance * h) co
 	return comps.front();
 }
 
-void Rewardable::Reward::loadComponents(std::vector<Component> & comps,
-								 const CGHeroInstance * h) const
+void Rewardable::Reward::loadComponents(std::vector<Component> & comps, const CGHeroInstance * h) const
 {
 	for (auto comp : extraComponents)
 		comps.push_back(comp);
@@ -71,14 +80,13 @@ void Rewardable::Reward::loadComponents(std::vector<Component> & comps,
 	}
 	
 	if (heroExperience)
-	{
-		comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast<si32>(h->calculateXp(heroExperience)), 0);
-	}
+		comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast<si32>(h ? h->calculateXp(heroExperience) : heroExperience), 0);
+
 	if (heroLevel)
 		comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0);
 
 	if (manaDiff || manaPercentage >= 0)
-		comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, calculateManaPoints(h) - h->mana, 0);
+		comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, h ? (calculateManaPoints(h) - h->mana) : manaDiff, 0);
 
 	for (size_t i=0; i<primary.size(); i++)
 	{

+ 34 - 4
lib/rewardable/Reward.h

@@ -27,6 +27,34 @@ namespace Rewardable
 struct Reward;
 using RewardsList = std::vector<std::shared_ptr<Rewardable::Reward>>;
 
+struct RewardRevealTiles
+{
+	/// Reveal distance, if not positive - reveal entire map
+	int radius;
+	/// Reveal score of terrains with "surface" flag set
+	int scoreSurface;
+	/// Reveal score of terrains with "subterra" flag set
+	int scoreSubterra;
+	/// Reveal score of terrains with "water" flag set
+	int scoreWater;
+	/// Reveal score of terrains with "rock" flag set
+	int scoreRock;
+	/// If set, then terrain will be instead hidden for all enemies (Cover of Darkness)
+	bool hide;
+
+	void serializeJson(JsonSerializeFormat & handler);
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & radius;
+		h & scoreSurface;
+		h & scoreSubterra;
+		h & scoreWater;
+		h & scoreRock;
+		h & hide;
+	}
+};
+
 /// Reward that can be granted to a hero
 /// NOTE: eventually should replace seer hut rewards and events/pandoras
 struct DLL_LINKAGE Reward final
@@ -74,12 +102,14 @@ struct DLL_LINKAGE Reward final
 	/// list of components that will be added to reward description. First entry in list will override displayed component
 	std::vector<Component> extraComponents;
 
+	std::optional<RewardRevealTiles> revealTiles;
+
 	/// if set to true, object will be removed after granting reward
 	bool removeObject;
 
 	/// Generates list of components that describes reward for a specific hero
-	void loadComponents(std::vector<Component> & comps,
-								const CGHeroInstance * h) const;
+	/// If hero is nullptr, then rewards will be generated without accounting for hero
+	void loadComponents(std::vector<Component> & comps, const CGHeroInstance * h) const;
 	
 	Component getDisplayedComponent(const CGHeroInstance * h) const;
 
@@ -107,8 +137,8 @@ struct DLL_LINKAGE Reward final
 		h & spells;
 		h & creatures;
 		h & creaturesChange;
-		if(version >= 821)
-			h & spellCast;
+		h & revealTiles;
+		h & spellCast;
 	}
 	
 	void serializeJson(JsonSerializeFormat & handler);

+ 2 - 2
scripting/lua/api/netpacks/SetResources.cpp

@@ -102,7 +102,7 @@ int SetResourcesProxy::getAmount(lua_State * L)
 	if(!S.tryGet(1, object))
 		return S.retVoid();
 
-	auto type = EGameResID::INVALID;
+	auto type = EGameResID::NONE;
 
 	if(!S.tryGet(2, type))
 		return S.retVoid();
@@ -122,7 +122,7 @@ int SetResourcesProxy::setAmount(lua_State * L)
 	if(!S.tryGet(1, object))
 		return S.retVoid();
 
-	auto type = EGameResID::INVALID;
+	auto type = EGameResID::NONE;
 
 	if(!S.tryGet(2, type))
 		return S.retVoid();

+ 20 - 21
server/CGameHandler.cpp

@@ -858,7 +858,7 @@ void CGameHandler::onNewTurn()
 			if (player != PlayerColor::NEUTRAL) //do not reveal fow for neutral player
 			{
 				FoWChange fw;
-				fw.mode = 1;
+				fw.mode = ETileVisibility::REVEALED;
 				fw.player = player;
 				// find all hidden tiles
 				const auto fow = getPlayerTeam(player)->fogOfWarMap;
@@ -879,7 +879,7 @@ void CGameHandler::onNewTurn()
 			{
 				if (getPlayerStatus(player.first) == EPlayerStatus::INGAME &&
 					getPlayerRelations(player.first, t->tempOwner) == PlayerRelations::ENEMIES)
-					changeFogOfWar(t->visitablePos(), t->getBonusLocalFirst(Selector::type()(BonusType::DARKNESS))->val, player.first, true);
+					changeFogOfWar(t->visitablePos(), t->getBonusLocalFirst(Selector::type()(BonusType::DARKNESS))->val, player.first, ETileVisibility::HIDDEN);
 			}
 		}
 	}
@@ -1174,7 +1174,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 		{
 			obj->onHeroLeave(h);
 		}
-		this->getTilesInRange(tmh.fowRevealed, h->getSightCenter()+(tmh.end-tmh.start), h->getSightRadius(), h->tempOwner, 1);
+		this->getTilesInRange(tmh.fowRevealed, h->getSightCenter()+(tmh.end-tmh.start), h->getSightRadius(), ETileVisibility::HIDDEN, h->tempOwner);
 	};
 
 	auto doMove = [&](TryMoveHero::EResult result, EGuardLook lookForGuards,
@@ -1523,7 +1523,7 @@ void CGameHandler::giveHero(ObjectInstanceID id, PlayerColor player, ObjectInsta
 
 	//Reveal fow around new hero, especially released from Prison
 	auto h = getHero(id);
-	changeFogOfWar(h->pos, h->getSightRadius(), player, false);
+	changeFogOfWar(h->pos, h->getSightRadius(), player, ETileVisibility::REVEALED);
 }
 
 void CGameHandler::changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)
@@ -2387,11 +2387,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID,
 		processAfterBuiltStructure(builtID);
 
 	// now when everything is built - reveal tiles for lookout tower
-	FoWChange fw;
-	fw.player = t->tempOwner;
-	fw.mode = 1;
-	getTilesInRange(fw.tiles, t->getSightCenter(), t->getSightRadius(), t->tempOwner, 1);
-	sendAndApply(&fw);
+	changeFogOfWar(t->getSightCenter(), t->getSightRadius(), t->getOwner(), ETileVisibility::REVEALED);
 
 	if(t->visitingHero)
 		visitCastleObjects(t, t->visitingHero);
@@ -4061,10 +4057,7 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID)
 void CGameHandler::synchronizeArtifactHandlerLists()
 {
 	UpdateArtHandlerLists uahl;
-	uahl.treasures = VLC->arth->treasures;
-	uahl.minors = VLC->arth->minors;
-	uahl.majors = VLC->arth->majors;
-	uahl.relics = VLC->arth->relics;
+	uahl.allocatedArtifacts = VLC->arth->allocatedArtifacts;
 	sendAndApply(&uahl);
 }
 
@@ -4111,34 +4104,40 @@ void CGameHandler::removeAfterVisit(const CGObjectInstance *object)
 	assert("This function needs to be called during the object visit!");
 }
 
-void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide)
+void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode)
 {
 	std::unordered_set<int3> tiles;
-	getTilesInRange(tiles, center, radius, player, hide? -1 : 1);
-	if (hide)
+
+	if (mode == ETileVisibility::HIDDEN)
 	{
+		getTilesInRange(tiles, center, radius, ETileVisibility::REVEALED, player);
+
 		std::unordered_set<int3> observedTiles; //do not hide tiles observed by heroes. May lead to disastrous AI problems
 		auto p = getPlayerState(player);
 		for (auto h : p->heroes)
 		{
-			getTilesInRange(observedTiles, h->getSightCenter(), h->getSightRadius(), h->tempOwner, -1);
+			getTilesInRange(observedTiles, h->getSightCenter(), h->getSightRadius(), ETileVisibility::REVEALED, h->tempOwner);
 		}
 		for (auto t : p->towns)
 		{
-			getTilesInRange(observedTiles, t->getSightCenter(), t->getSightRadius(), t->tempOwner, -1);
+			getTilesInRange(observedTiles, t->getSightCenter(), t->getSightRadius(), ETileVisibility::REVEALED, t->tempOwner);
 		}
 		for (auto tile : observedTiles)
 			vstd::erase_if_present (tiles, tile);
 	}
-	changeFogOfWar(tiles, player, hide);
+	else
+	{
+		getTilesInRange(tiles, center, radius, ETileVisibility::HIDDEN, player);
+	}
+	changeFogOfWar(tiles, player, mode);
 }
 
-void CGameHandler::changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player, bool hide)
+void CGameHandler::changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode)
 {
 	FoWChange fow;
 	fow.tiles = tiles;
 	fow.player = player;
-	fow.mode = hide? 0 : 1;
+	fow.mode = mode;
 	sendAndApply(&fow);
 }
 

+ 2 - 2
server/CGameHandler.h

@@ -148,8 +148,8 @@ public:
 	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override;
 	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override;
 
-	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override;
-	void changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player, bool hide) override;
+	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override;
+	void changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player,ETileVisibility mode) override;
 	
 	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override;
 

+ 2 - 2
server/processors/PlayerMessageProcessor.cpp

@@ -346,7 +346,7 @@ void PlayerMessageProcessor::cheatDefeat(PlayerColor player)
 void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal)
 {
 	FoWChange fc;
-	fc.mode = reveal;
+	fc.mode = reveal ? ETileVisibility::REVEALED : ETileVisibility::HIDDEN;
 	fc.player = player;
 	const auto & fowMap = gameHandler->gameState()->getPlayerTeam(player)->fogOfWarMap;
 	const auto & mapSize = gameHandler->gameState()->getMapSize();
@@ -356,7 +356,7 @@ void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal)
 	for(int z = 0; z < mapSize.z; z++)
 		for(int x = 0; x < mapSize.x; x++)
 			for(int y = 0; y < mapSize.y; y++)
-				if(!(*fowMap)[z][x][y] || !fc.mode)
+				if(!(*fowMap)[z][x][y] || fc.mode == ETileVisibility::HIDDEN)
 					hlp_tab[lastUnc++] = int3(x, y, z);
 
 	fc.tiles.insert(hlp_tab, hlp_tab + lastUnc);

+ 2 - 2
test/mock/mock_IGameCallback.h

@@ -85,8 +85,8 @@ public:
 	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {}
 	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}
 	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {} //when two heroes meet on adventure map
-	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {}
-	void changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player, bool hide) override {}
+	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
+	void changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode) override {}
 	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}
 
 	///useful callback methods