Browse Source

Merge pull request #1486 from IvanSavenko/configurable_rewardables

Well, it works for my brand new objects with arbitrary bonus, so likely will work with everything else.
DjWarmonger 2 years ago
parent
commit
25ef065dbe

+ 3 - 2
AI/Nullkiller/AIUtility.cpp

@@ -314,8 +314,9 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj)
 		return false;
 
 	//TODO: allow polling of remaining creatures in dwelling
-	if(dynamic_cast<const CGVisitableOPW *>(obj)) // ensures future compatibility, unlike IDs
-		return true;
+	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
+		return rewardable->getResetDuration() == 7;
+
 	if(dynamic_cast<const CGDwelling *>(obj))
 		return true;
 	if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods

+ 9 - 6
AI/Nullkiller/Engine/AIMemory.cpp

@@ -70,12 +70,15 @@ void AIMemory::markObjectVisited(const CGObjectInstance * obj)
 		return;
 	
 	// TODO: maybe this logic belongs to CaptureObjects::shouldVisit
-	if(dynamic_cast<const CGVisitableOPH *>(obj)) //we may want to visit it with another hero
-		return;
-	
-	if(dynamic_cast<const CGBonusingObject *>(obj)) //or another time
-		return;
-	
+	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
+	{
+		if (rewardable->getVisitMode() == CRewardableObject::VISIT_HERO) //we may want to visit it with another hero
+			return;
+
+		if (rewardable->getVisitMode() == CRewardableObject::VISIT_BONUS) //or another time
+			return;
+	}
+
 	if(obj->ID == Obj::MONSTER)
 		return;
 

+ 14 - 12
AI/VCAI/VCAI.cpp

@@ -29,12 +29,6 @@
 
 extern FuzzyHelper * fh;
 
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CGVisitableOPW;
-
-VCMI_LIB_NAMESPACE_END
-
 const double SAFE_ATTACK_CONSTANT = 1.5;
 
 //one thread may be turn of AI and another will be handling a side effect for AI2
@@ -1606,12 +1600,19 @@ void VCAI::markObjectVisited(const CGObjectInstance * obj)
 {
 	if(!obj)
 		return;
-	if(dynamic_cast<const CGVisitableOPH *>(obj)) //we may want to visit it with another hero
-		return;
-	if(dynamic_cast<const CGBonusingObject *>(obj)) //or another time
-		return;
+
+	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj)) //we may want to visit it with another hero
+	{
+		if (rewardable->getVisitMode() == CRewardableObject::VISIT_HERO) //we may want to visit it with another hero
+			return;
+
+		if (rewardable->getVisitMode() == CRewardableObject::VISIT_BONUS) //or another time
+			return;
+	}
+
 	if(obj->ID == Obj::MONSTER)
 		return;
+
 	alreadyVisited.insert(obj);
 }
 
@@ -2742,8 +2743,9 @@ bool AIStatus::channelProbing()
 bool isWeeklyRevisitable(const CGObjectInstance * obj)
 {
 	//TODO: allow polling of remaining creatures in dwelling
-	if(dynamic_cast<const CGVisitableOPW *>(obj)) // ensures future compatibility, unlike IDs
-		return true;
+	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
+		return rewardable->getResetDuration() == 7;
+
 	if(dynamic_cast<const CGDwelling *>(obj))
 		return true;
 	if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods

+ 5 - 1
config/gameConfig.json

@@ -51,7 +51,11 @@
 		"config/objects/moddables.json",
 		"config/objects/creatureBanks.json",
 		"config/objects/dwellings.json",
-		"config/objects/rewardable.json"
+		"config/objects/rewardableOncePerWeek.json",
+		"config/objects/rewardablePickable.json",
+		"config/objects/rewardableOnceVisitable.json",
+		"config/objects/rewardableOncePerHero.json",
+		"config/objects/rewardableBonusing.json"
 	],
 
 	"artifacts" :

+ 0 - 22
config/objects/generic.json

@@ -597,28 +597,6 @@
 			}
 		}
 	},
-	"magicWell" : {
-		"index" :49,
-		"handler" : "magicWell",
-		"base" : {
-			"sounds" : {
-				"visit" : ["FAERIE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 250,
-				"rmg" : {
-					"zoneLimit" : 1,
-					"value"		: 250,
-					"rarity"	: 100
-				}
-			},
-			"objectWoG" : { "index" : 1} // WoG object? Present on VCMI_Test 2011b
-		}
-	},
-
 	/// Random objects
 	"randomTown"					: { "index" :77,  "handler": "randomTown", "types" : { "object" : { "index" : 0} }  },
 	"randomHero" : {

+ 0 - 716
config/objects/rewardable.json

@@ -1,716 +0,0 @@
-{
-	/// These are objects that covered by concept of "configurable object"
-	/// Most or even all of their configuration located in this file
-	"magicSpring" : {//magic source
-		"index" : 48,
-		"handler": "magicSpring",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPFOUN"],
-				"visit" : ["FAERIE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 500//,
-				//"rmg" : {
-				//	"zoneLimit"	: 1,
-				//	"value"		: 500,
-				//	"rarity"	: 50
-				//}
-				//banned due to problems with 2 viistable offsets
-			}
-		}
-	},
-
-	"mysticalGarden" : {
-		"index" : 55,
-		"handler": "oncePerWeek",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPLEPR"],
-				"visit" : ["EXPERNCE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 500,
-				"rmg" : {
-					"value"		: 500,
-					"rarity"	: 50
-				}
-			}
-		}
-	},
-	"windmill" :{
-		"index" : 112,
-		"handler": "oncePerWeek",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPWIND"],
-				"visit" : ["GENIE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"value"		: 1500,
-					"rarity"	: 80
-				}
-			}
-		}
-	},
-	"waterWheel" : {
-		"index" : 109,
-		"handler": "oncePerWeek",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPMILL"],
-				"visit" : ["GENIE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 750,
-				"rmg" : {
-					"value"		: 750,
-					"rarity"	: 50
-				}
-			}
-		}
-	},
-	
-	"leanTo" :{
-		"index" : 39,
-		"handler": "onceVisitable",
-		"base" : {
-			"sounds" : {
-				"visit" : ["GENIE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 500,
-				"rmg" : {
-					"value"		: 500,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"corpse" :{
-		"index" : 22,
-		"handler": "onceVisitable",
-		"base" : {
-			"sounds" : {
-				"visit" : ["MYSTERY"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 500,
-				"rmg" : {
-					"value"		: 500,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"wagon" :{
-		"index" : 105,
-		"handler": "onceVisitable",
-		"base" : {
-			"sounds" : {
-				"visit" : ["GENIE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 500,
-				"rmg" : {
-					"value"		: 500,
-					"rarity"	: 50
-				}
-			}
-		}
-	},
-	"warriorTomb" : {
-		"index" : 108,
-		"handler": "onceVisitable",
-		"base" : {
-			"sounds" : {
-				"visit" : ["GRAVEYARD"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 6000,
-				"rmg" : {
-					"value"		: 6000,
-					"rarity"	: 20
-				}
-			}
-		}
-	},
-
-	"campfire" :{
-		"index" : 12,
-		"handler": "pickable",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPCAMP"],
-				"visit" : ["EXPERNCE"],
-				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 2000,
-				"rmg" : {
-					"value"		: 2000,
-					"rarity"	: 500
-				}
-			}
-		}
-	},
-	"flotsam" :{
-		"index" : 29,
-		"handler": "pickable",
-		"base" : {
-			"sounds" : {
-				"visit" : ["GENIE"],
-				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 2000,
-				"rmg" : {
-					"value"		: 2000,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"seaChest" :{
-		"index" : 82,
-		"handler": "pickable",
-		"base" : {
-			"sounds" : {
-				"visit" : ["CHEST"],
-				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"value"		: 1500,
-					"rarity"	: 500
-				}
-			}
-		}
-	},
-	"shipwreckSurvivor" : {
-		"index" : 86,
-		"handler": "pickable",
-		"base" : {
-			"sounds" : {
-				"visit" : ["TREASURE"],
-				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"value"		: 1500,
-					"rarity"	: 50
-				}
-			}
-		}
-	},
-	"treasureChest" : {
-		"index" : 101,
-		"handler": "pickable",
-		"base" : {
-			"sounds" : {
-				"visit" : ["CHEST"],
-				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"value"		: 1500,
-					"rarity"	: 1000
-				}
-			}
-		}
-	},
-
-	"arena" : {
-		"index" : 4,
-		"handler": "oncePerHero",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPAREN"],
-				"visit" : ["NOMAD"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 3000,
-				"rmg" : {
-					"value"		: 3000,
-					"rarity"	: 50
-				}
-			}
-		}
-	},
-	"marlettoTower" : {
-		"index" : 23,
-		"handler": "oncePerHero",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPSWAR"],
-				"visit" : ["NOMAD"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"value"		: 1500,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"gardenOfRevelation" : {
-		"index" : 32,
-		"handler": "oncePerHero",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPGARD"],
-				"visit" : ["GETPROTECTION"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"value"		: 1500,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"libraryOfEnlightenment" : {
-		"index" : 41,
-		"handler": "oncePerHero",
-		"base" : {
-			"sounds" : {
-				"visit" : ["GAZEBO"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 12000,
-				"rmg" : {
-					"value"		: 12000,
-					"rarity"	: 20
-				}
-			}
-		}
-	},
-	"mercenaryCamp" : {
-		"index" : 51,
-		"handler": "oncePerHero",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPMERC"],
-				"visit" : ["NOMAD"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"value"		: 1500,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"starAxis" :{
-		"index" : 61,
-		"handler": "oncePerHero",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPSTAR"],
-				"visit" : ["GAZEBO"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"value"		: 1500,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"treeOfKnowledge" : {
-		"index" : 102,
-		"handler": "oncePerHero",
-		"base" : {
-			"sounds" : {
-				"visit" : ["GAZEBO"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 2500,
-				"rmg" : {
-					"mapLimit"	: 100,
-					"value"		: 2500,
-					"rarity"	: 50
-				}
-			}
-		}
-	},
-	"schoolOfMagic" : {
-		"index" : 47,
-		"handler": "oncePerHero",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPMAGI"],
-				"visit" : ["FAERIE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1000,
-				"rmg" : {
-					"value"		: 1000,
-					"rarity"	: 50
-				}
-			}
-		}
-	},
-	"schoolOfWar" : {
-		"index" : 107,
-		"handler": "oncePerHero",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPSWAR"],
-				"visit" : ["MILITARY"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 1000,
-				"rmg" : {
-					"value"		: 1000,
-					"rarity"	: 50
-				}
-			}
-		}
-	},
-	"learningStone" : {
-		"index" : 100,
-		"handler": "oncePerHero",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPLEAR"],
-				"visit" : ["GAZEBO"]
-			}
-		},
-		"types" : {
-			"object" : { 
-				"index" : 0,
-				"aiValue" : 1500,
-				"rmg" : {
-					"value"		: 1500,
-					"rarity"	: 200
-				} 
-			},
-			"objectWoG" : { "index" : 1 } // WoG object? Present on VCMI_Tests 2011
-		}
-	},
-
-	"buoy" : {
-		"index" : 11,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPBUOY"],
-				"visit" : ["MORALE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-					"value"		: 100,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"swanPond" : {
-		"index" : 14,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"visit" : ["LUCK"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 100,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"faerieRing" : {
-		"index" : 28,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPFAER"],
-				"visit" : ["LUCK"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 100,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"fountainOfFortune" : {
-		"index" : 30,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPFOUN"],
-				"visit" : ["LUCK"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 100,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"fountainOfYouth" : {
-		"index" : 31,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPFALL"],
-				"visit" : ["MORALE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 100,
-					"rarity"	: 50
-				}
-			}
-		}
-	},
-	"idolOfFortune" : {
-		"index" : 38,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"visit" : ["LUCK"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 100,
-					"rarity"	: 100
-				}
-			},
-			"object1" : {//WoG?
-				"index" : 1
-			}
-		}
-	},
-	"mermaids" : {
-		"index" : 52,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"visit" : ["LUCK"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-					"value"		: 100,
-					"rarity"	: 20
-				}
-			}
-		}
-	},
-	"oasis" : {
-		"index" : 56,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"visit" : ["MORALE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 100,
-					"rarity"	: 50
-				}
-			}
-		}
-	},
-	"stables" : {
-		"index" : 94,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPHORS"],
-				"visit" : ["STORE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 200,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 200,
-					"rarity"	: 40
-				}
-			}
-		}
-	},
-	"temple" : {
-		"index" : 96,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPSANC"],
-				"visit" : ["TEMPLE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 100,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"rallyFlag" : {
-		"index" : 64,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"ambient" : ["LOOPFLAG"],
-				"visit" : ["MORALE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 100,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 100,
-					"rarity"	: 100
-				}
-			}
-		}
-	},
-	"wateringHole" : {//waters
-		"index" : 110,
-		"handler": "bonusingObject",
-		"base" : {
-			"sounds" : {
-				"visit" : ["MORALE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 500,
-				"rmg" : {
-					"zoneLimit"	: 1,
-					"value"		: 500,
-					"rarity"	: 50
-				}
-			} 
-		}
-	}
-}

+ 457 - 0
config/objects/rewardableBonusing.json

@@ -0,0 +1,457 @@
+{
+	/// These are objects that covered by concept of "configurable object" and have their entire configuration in this config
+
+	"buoy" : {
+		"index" : 11,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPBUOY"],
+				"visit" : ["MORALE"]
+			}
+		},
+		"types" : {
+			"buoy" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+					"value"		: 100,
+					"rarity"	: 100
+				},
+				
+				"blockedVisitable" : true,
+				"onVisitedMessage" : 22,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 21,
+						"bonuses" : [ { "type" : "MORALE", "val" : 1, "duration" : "ONE_BATTLE", "description" : 94 } ]
+					}
+				]
+			}	
+		}
+	},
+	"swanPond" : {
+		"index" : 14,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["LUCK"]
+			}
+		},
+		"types" : {
+			"swanPond" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 100,
+					"rarity"	: 100
+				},
+				
+				"onVisitedMessage" : 30,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 29,
+						"movePercentage" : 0,
+						"bonuses" : [ { "type" : "LUCK", "val" : 2, "duration" : "ONE_BATTLE", "description" : 67 } ]
+					}
+				]
+			}
+		}
+	},
+	"faerieRing" : {
+		"index" : 28,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPFAER"],
+				"visit" : ["LUCK"]
+			}
+		},
+		"types" : {
+			"faerieRing" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 100,
+					"rarity"	: 100
+				},
+				
+				"onVisitedMessage" : 50,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 49,
+						"bonuses" : [ { "type" : "LUCK", "val" : 1, "duration" : "ONE_BATTLE", "description" : 71 } ]
+					}
+				]
+			}
+		}
+	},
+	"fountainOfFortune" : {
+		"index" : 30,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPFOUN"],
+				"visit" : ["LUCK"]
+			}
+		},
+		"types" : {
+			"fountainOfFortune" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 100,
+					"rarity"	: 100
+				},
+				
+				"onVisitedMessage" : 56,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"resetParameters" : {
+					"period" : 7,
+					"rewards" : true
+				},
+
+				"rewards" : [
+					{
+						"message" : 55,
+						"appearChance" : { "min" : 0, "max" : 25 },
+						"bonuses" : [ { "type" : "LUCK", "val" : -1, "duration" : "ONE_BATTLE", "description" : 69 } ] // NOTE: strings has %s placeholder for morale value
+					},
+					{
+						"message" : 55,
+						"appearChance" : { "min" : 25, "max" : 50 },
+						"bonuses" : [ { "type" : "LUCK", "val" : 1, "duration" : "ONE_BATTLE", "description" : 69 } ] // NOTE: strings has %s placeholder for morale value
+					},
+					{
+						"message" : 55,
+						"appearChance" : { "min" : 50, "max" : 75 },
+						"bonuses" : [ { "type" : "LUCK", "val" : 2, "duration" : "ONE_BATTLE", "description" : 69 } ] // NOTE: strings has %s placeholder for morale value
+					},
+					{
+						"message" : 55,
+						"appearChance" : { "min" : 75, "max" : 100 },
+						"bonuses" : [ { "type" : "LUCK", "val" : 3, "duration" : "ONE_BATTLE", "description" : 69 } ] // NOTE: strings has %s placeholder for morale value
+					},
+				]
+			}
+		}
+	},
+	"fountainOfYouth" : {
+		"index" : 31,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPFALL"],
+				"visit" : ["MORALE"]
+			}
+		},
+		"types" : {
+			"fountainOfYouth" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 100,
+					"rarity"	: 50
+				},
+				
+				"onVisitedMessage" : 58,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 57,
+						"movePoints" : 400,
+						"bonuses" : [ { "type" : "MORALE", "val" : 1, "duration" : "ONE_BATTLE", "description" : 103 } ]
+					}
+				]
+			}
+		}
+	},
+	"idolOfFortune" : {
+		"index" : 38,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["LUCK"]
+			}
+		},
+		"types" : {
+			"idolOfFortune" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 100,
+					"rarity"	: 100
+				},
+				
+				"onVisitedMessage" : 63,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 62,
+						"limiter" : { 
+							"anyOf" : [
+								{ "dayOfWeek" : 1 },
+								{ "dayOfWeek" : 3 },
+								{ "dayOfWeek" : 5 }
+							]
+						},
+						"bonuses" : [ { "type" : "LUCK", "val" : 1, "duration" : "ONE_BATTLE", "description" : 68 } ]
+					},
+					{
+						"message" : 62,
+						"limiter" : { 
+							"anyOf" : [
+								{ "dayOfWeek" : 2 },
+								{ "dayOfWeek" : 4 },
+								{ "dayOfWeek" : 6 }
+							]
+						},
+						"bonuses" : [ { "type" : "MORALE", "val" : 1, "duration" : "ONE_BATTLE", "description" : 68 } ]
+					},
+					{
+						"message" : 62,
+						"limiter" : { "dayOfWeek" : 7 },
+						"bonuses" : [ 
+							{ "type" : "MORALE", "val" : 1, "duration" : "ONE_BATTLE", "description" : 68 },
+							{ "type" : "LUCK", "val" : 1, "duration" : "ONE_BATTLE", "description" : 68 }  
+						]
+					}
+				]
+			}
+		}
+	},
+	"mermaids" : {
+		"index" : 52,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["LUCK"]
+			}
+		},
+		"types" : {
+			"mermaids" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+					"value"		: 100,
+					"rarity"	: 20
+				},
+				
+				"onVisitedMessage" : 82,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 83,
+						"bonuses" : [ { "type" : "LUCK", "val" : 1, "duration" : "ONE_BATTLE", "description" : 72 } ]
+					}
+				]
+			}
+		}
+	},
+	"oasis" : {
+		"index" : 56,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["MORALE"]
+			}
+		},
+		"types" : {
+			"oasis" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 100,
+					"rarity"	: 50
+				},
+				
+				"onVisitedMessage" : 95,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 94,
+						"movePoints" : 800,
+						"bonuses" : [ { "type" : "MORALE", "val" : 1, "duration" : "ONE_BATTLE", "description" : 95 } ]
+					}
+				]
+			}
+		}
+	},
+	"stables" : {
+		"index" : 94,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPHORS"],
+				"visit" : ["STORE"]
+			}
+		},
+		"types" : {
+			"stables" : {
+				"index" : 0,
+				"aiValue" : 200,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 200,
+					"rarity"	: 40
+				},
+				
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+
+				"onVisited" : [
+					{
+						"message" : 139,
+						"limiter" : {
+							"creatures" : [ { "type" : "cavalier", "amount" : 1 } ],
+						},
+						"changeCreatures" : {
+							"cavalier" : "champion"
+						}
+					},
+					{
+						"message" : 136
+					},
+				],
+
+				"rewards" : [
+					{
+						"limiter" : {
+							"creatures" : [ { "type" : "cavalier", "amount" : 1 } ],
+						},
+						"message" : 138,
+						"movePoints" : 400,
+						"bonuses" : [ { "type" : "LAND_MOVEMENT", "val" : 400, "duration" : "ONE_WEEK"} ],
+						"changeCreatures" : {
+							"cavalier" : "champion"
+						}
+					},
+					{
+						"message" : 137,
+						"movePoints" : 400,
+						"bonuses" : [ { "type" : "LAND_MOVEMENT", "val" : 400, "duration" : "ONE_WEEK"} ]
+					}
+				]
+			}
+		}
+	},
+	"temple" : {
+		"index" : 96,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPSANC"],
+				"visit" : ["TEMPLE"]
+			}
+		},
+		"types" : {
+			"temple" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 100,
+					"rarity"	: 100
+				},
+				
+				"onVisitedMessage" : 141,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 140,
+						"limiter" : { "dayOfWeek" : 7 },
+						"bonuses" : [ { "type" : "MORALE", "val" : 2, "duration" : "ONE_BATTLE", "description" : 97 } ]
+					},
+					{
+						"message" : 140,
+						"bonuses" : [ { "type" : "MORALE", "val" : 1, "duration" : "ONE_BATTLE", "description" : 96 } ]
+					}
+				]
+			}
+		}
+	},
+	"rallyFlag" : {
+		"index" : 64,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPFLAG"],
+				"visit" : ["MORALE"]
+			}
+		},
+		"types" : {
+			"rallyFlag" : {
+				"index" : 0,
+				"aiValue" : 100,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 100,
+					"rarity"	: 100
+				},
+				
+				"onVisitedMessage" : 111,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 110,
+						"movePoints" : 400,
+						"bonuses" : [ 
+							{ "type" : "MORALE", "val" : 1, "duration" : "ONE_BATTLE", "description" : 102 },
+							{ "type" : "LUCK", "val" : 1, "duration" : "ONE_BATTLE", "description" : 102 }
+						]
+					}
+				]
+			}
+		}
+	},
+	"wateringHole" : {
+		"index" : 110,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["MORALE"]
+			}
+		},
+		"types" : {
+			"wateringHole" : {
+				"index" : 0,
+				"aiValue" : 500,
+				"rmg" : {
+					"zoneLimit"	: 1,
+					"value"		: 500,
+					"rarity"	: 50
+				},
+				
+				"onVisitedMessage" : 167,
+				"visitMode" : "bonus",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 166,
+						"movePoints" : 400,
+						"bonuses" : [ { "type" : "MORALE", "val" : 1, "duration" : "ONE_BATTLE", "description" : 100 } ]
+					}
+				]
+			} 
+		}
+	}
+}

+ 364 - 0
config/objects/rewardableOncePerHero.json

@@ -0,0 +1,364 @@
+{
+	/// These are objects that covered by concept of "configurable object" and have their entire configuration in this config
+
+	"arena" : {
+		"index" : 4,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPAREN"],
+				"visit" : ["NOMAD"]
+			}
+		},
+		"types" : {
+			"arena" : {
+				"index" : 0,
+				"aiValue" : 3000,
+				"rmg" : {
+					"value"		: 3000,
+					"rarity"	: 50
+				},
+				
+				"onSelectMessage" : 0,
+				"onVisitedMessage" : 1,
+				"visitMode" : "hero",
+				"selectMode" : "selectPlayer",
+				"rewards" : [
+					{
+						"primary" : { "attack" : 2 }
+					},
+					{
+						"primary" : { "defence" : 2 }
+					}
+				]
+			}
+		}
+	},
+	"marlettoTower" : {
+		"index" : 23,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPSWAR"],
+				"visit" : ["NOMAD"]
+			}
+		},
+		"types" : {
+			"marlettoTower" : {
+				"index" : 0,
+				"aiValue" : 1500,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 100
+				},
+
+				"onVisitedMessage" : 40,
+				"visitMode" : "hero",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 39,
+						"primary" : { "defence" : 1 }
+					}
+				]
+			}
+		}
+	},
+	"gardenOfRevelation" : {
+		"index" : 32,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPGARD"],
+				"visit" : ["GETPROTECTION"]
+			}
+		},
+		"types" : {
+			"gardenOfRevelation" : {
+				"index" : 0,
+				"aiValue" : 1500,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 100
+				},
+				
+				"onVisitedMessage" : 60,
+				"visitMode" : "hero",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 59,
+						"primary" : { "knowledge" : 1 }
+					}
+				]
+			}
+		}
+	},
+	"libraryOfEnlightenment" : {
+		"index" : 41,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["GAZEBO"]
+			}
+		},
+		"types" : {
+			"libraryOfEnlightenment" : {
+				"index" : 0,
+				"aiValue" : 12000,
+				"rmg" : {
+					"value"		: 12000,
+					"rarity"	: 20
+				},
+				
+				"onVisitedMessage" : 67,
+				"onEmptyMessage" : 68,
+				"visitMode" : "hero",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"limiter" : {
+							"anyOf" : [
+								{ "heroLevel" : 10 },
+								{ "heroLevel" :  8, "secondary" : { "diplomacy" : 1 } },
+								{ "heroLevel" :  6, "secondary" : { "diplomacy" : 2 } },
+								{ "heroLevel" :  4, "secondary" : { "diplomacy" : 3 } }
+							]
+						},
+						"message" : 59,
+						"primary" : { 
+							"attack" : 2,
+							"defence" : 2,
+							"spellpower" : 2,
+							"knowledge" : 2
+						}
+					}
+				]
+			}
+		}
+	},
+	"mercenaryCamp" : {
+		"index" : 51,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPMERC"],
+				"visit" : ["NOMAD"]
+			}
+		},
+		"types" : {
+			"mercenaryCamp" : {
+				"index" : 0,
+				"aiValue" : 1500,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 100
+				},
+			
+				"onVisitedMessage" : 81,
+				"visitMode" : "hero",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 80,
+						"primary" : { "attack" : 1 }
+					}
+				]
+			}
+		}
+	},
+	"starAxis" :{
+		"index" : 61,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPSTAR"],
+				"visit" : ["GAZEBO"]
+			}
+		},
+		"types" : {
+			"starAxis" : {
+				"index" : 0,
+				"aiValue" : 1500,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 100
+				},
+				
+				"onVisitedMessage" : 101,
+				"visitMode" : "hero",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 100,
+						"primary" : { "spellpower" : 1 }
+					}
+				]
+			}
+		}
+	},
+	"treeOfKnowledge" : {
+		"index" : 102,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["GAZEBO"]
+			}
+		},
+		"types" : {
+			"treeOfKnowledge" : {
+				"index" : 0,
+				"aiValue" : 2500,
+				"rmg" : {
+					"mapLimit"	: 100,
+					"value"		: 2500,
+					"rarity"	: 50
+				},
+
+				"onEmpty" : [
+					{
+						"message" : 150,
+						"appearChance" : { "min" : 34, "max" : 67 }
+					},
+					{
+						"message" : 152,
+						"appearChance" : { "min" : 67, "max" : 67 }
+					}
+				],
+				"onVisitedMessage" : 147,
+				"visitMode" : "hero",
+				"selectMode" : "selectFirst",
+				"canRefuse" : true,
+				"rewards" : [
+					{
+						"message" : 148,
+						"appearChance" : { "max" : 34 },
+						"gainedLevels" : 1
+					},
+					{
+						"message" : 149,
+						"appearChance" : { "min" : 34, "max" : 67 },
+						"limiter" : { "resources" : { "gold" : 2000 } },
+						"resources" : { "gold" : -2000 },
+						"gainedLevels" : 1
+					},
+					{
+						"message" : 151,
+						"appearChance" : { "min" : 67 },
+						"limiter" : { "resources" : { "gems" : 10 } },
+						"resources" : { "gems" : -10 },
+						"gainedLevels" : 1
+					},
+				]	
+			}
+		}
+	},
+	"schoolOfMagic" : {
+		"index" : 47,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPMAGI"],
+				"visit" : ["FAERIE"]
+			}
+		},
+		"types" : {
+			"schoolOfMagic" : {
+				"index" : 0,
+				"aiValue" : 1000,
+				"rmg" : {
+					"value"		: 1000,
+					"rarity"	: 50
+				},
+				
+				"onSelectMessage" : 71,
+				"onVisitedMessage" : 72,
+				"onEmptyMessage" : 73,
+				"visitMode" : "hero",
+				"selectMode" : "selectPlayer",
+				"canRefuse" : true,
+				"rewards" : [
+					{
+						"limiter" : { "resources" : { "gold" : 1000 } },
+						"resources" : { "gold" : -1000 },
+						"primary" : { "spellpower" : 1 }
+					},
+					{
+						"limiter" : { "resources" : { "gold" : 1000 } },
+						"resources" : { "gold" : -1000 },
+						"primary" : { "knowledge" : 1 }
+					}
+				]
+			}
+		}
+	},
+	"schoolOfWar" : {
+		"index" : 107,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPSWAR"],
+				"visit" : ["MILITARY"]
+			}
+		},
+		"types" : {
+			"schoolOfWar" : {
+				"index" : 0,
+				"aiValue" : 1000,
+				"rmg" : {
+					"value"		: 1000,
+					"rarity"	: 50
+				},
+
+				"onSelectMessage" : 158,
+				"onVisitedMessage" : 159,
+				"onEmptyMessage" : 160,
+				"visitMode" : "hero",
+				"selectMode" : "selectPlayer",
+				"canRefuse" : true,
+				"rewards" : [
+					{
+						"limiter" : { "resources" : { "gold" : 1000 } },
+						"resources" : { "gold" : -1000 },
+						"primary" : { "attack" : 1 }
+					},
+					{
+						"limiter" : { "resources" : { "gold" : 1000 } },
+						"resources" : { "gold" : -1000 },
+						"primary" : { "defence" : 1 }
+					}
+				]	
+			}
+		}
+	},
+	"learningStone" : {
+		"index" : 100,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPLEAR"],
+				"visit" : ["GAZEBO"]
+			}
+		},
+		"types" : {
+			"learningStone" : { 
+				"index" : 0,
+				"aiValue" : 1500,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 200
+				},
+
+				"onVisitedMessage" : 144,
+				"visitMode" : "hero",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 143,
+						"heroExperience" : 1000
+					}
+				]
+			}
+		}
+	},
+}

+ 199 - 0
config/objects/rewardableOncePerWeek.json

@@ -0,0 +1,199 @@
+{
+	/// 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
+				},
+				
+				"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
+				//},
+				
+				"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",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPLEPR"],
+				"visit" : ["EXPERNCE"]
+			}
+		},
+		"types" : {
+			"mysticalGarden" : {
+				"index" : 0,
+				"aiValue" : 500,
+				"rmg" : {
+					"value"		: 500,
+					"rarity"	: 50
+				},
+				
+				"onVisitedMessage" : 93,
+				"resetParameters" : {
+					"period" : 7,
+					"visitors" : true,
+					"rewards" : true
+				},
+				"visitMode" : "once",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 92,
+						"appearChance" : { "max" : 50 },
+						"resources" : { "gems" : 5 }
+					},
+					{
+						"message" : 92,
+						"appearChance" : { "min" : 50 },
+						"resources" : { "gold" : 500 }
+					}
+				]
+			}
+		}
+	},
+	"windmill" :{
+		"index" : 112,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPWIND"],
+				"visit" : ["GENIE"]
+			}
+		},
+		"types" : {
+			"windmill" : {
+				"index" : 0,
+				"aiValue" : 1500,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 80
+				},
+				
+				"onVisitedMessage" : 169,
+				"resetParameters" : {
+					"period" : 7,
+					"visitors" : true,
+					"rewards" : true
+				},
+				"visitMode" : "once",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 170,
+						"resources" : [
+							{
+								"list" : [ "ore", "mercury", "gems", "sulfur", "crystal" ],
+								"min" : 3,
+								"max" : 6
+							}
+						]
+					}
+				]
+			}
+		}
+	},
+	"waterWheel" : {
+		"index" : 109,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPMILL"],
+				"visit" : ["GENIE"]
+			}
+		},
+		"types" : {
+			"waterWheel" : {
+				"index" : 0,
+				"aiValue" : 750,
+				"rmg" : {
+					"value"		: 750,
+					"rarity"	: 50
+				},
+				
+				"onVisitedMessage" : 165,
+				"resetParameters" : {
+					"period" : 7,
+					"visitors" : true
+				},
+				"visitMode" : "once",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"limiter" : { "daysPassed" : 8 },
+						"message" : 164,
+						"resources" : { "gold" : 1000 }
+					},
+					{
+						"message" : 164,
+						"resources" : { "gold" : 500 }
+					}
+				]
+			}
+		}
+	}
+}

+ 185 - 0
config/objects/rewardableOnceVisitable.json

@@ -0,0 +1,185 @@
+{
+	/// These are objects that covered by concept of "configurable object" and have their entire configuration in this config
+
+	"leanTo" :{
+		"index" : 39,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["GENIE"]
+			}
+		},
+		"types" : {
+			"leanTo" : {
+				"index" : 0,
+				"aiValue" : 500,
+				"rmg" : {
+					"value"		: 500,
+					"rarity"	: 100
+				},
+				
+				"onVisitedMessage" : 65,
+				"visitMode" : "once",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 64,
+						"resources" : [
+							{
+								"list" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ],
+								"min" : 1,
+								"max" : 5
+							}
+						]
+					}
+				]				
+			}
+		}
+	},
+	"corpse" :{
+		"index" : 22,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["MYSTERY"]
+			}
+		},
+		"types" : {
+			"corpse" : {
+				"index" : 0,
+				"aiValue" : 500,
+				"rmg" : {
+					"value"		: 500,
+					"rarity"	: 100
+				},
+				
+				"onVisitedMessage" : 38,
+				"blockedVisitable" : true,
+				"visitMode" : "once",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"appearChance" : { "max" : 10 },
+						"message" : 37,
+						"artifacts" : [ { "class" : "TREASURE" } ]
+					},
+					{
+						"appearChance" : { "min" : 10, "max" : 20 },
+						"message" : 37,
+						"artifacts" : [ { "class" : "MINOR" } ]
+					},
+					{
+						"appearChance" : { "min" : 20, "max" : 100 },
+						"message" : 38,
+					}
+				]
+			}
+		}
+	},
+	"wagon" :{
+		"index" : 105,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["GENIE"]
+			}
+		},
+		"types" : {
+			"wagon" : {
+				"index" : 0,
+				"aiValue" : 500,
+				"rmg" : {
+					"value"		: 500,
+					"rarity"	: 50
+				},
+				
+				"onVisitedMessage" : 156,
+				"visitMode" : "once",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"appearChance" : { "max" : 20 },
+						"message" : 155,
+						"artifacts" : [ { "class" : "TREASURE" } ]
+					},
+					{
+						"appearChance" : { "min" : 20, "max" : 40 },
+						"message" : 155,
+						"artifacts" : [ { "class" : "MINOR" } ]
+					},
+					{
+						"message" : 154,
+						"appearChance" : { "min" : 40, "max" : 90 },
+						"resources" : [
+							{
+								"list" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ],
+								"min" : 2,
+								"max" : 5
+							},
+						]
+					},
+					{
+						"appearChance" : { "min" : 90, "max" : 100 },
+						"message" : 156,
+					}
+				]
+			}
+		}
+	},
+	"warriorTomb" : {
+		"index" : 108,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["GRAVEYARD"]
+			}
+		},
+		"types" : {
+			"warriorTomb" : {
+				"index" : 0,
+				"aiValue" : 6000,
+				"rmg" : {
+					"value"		: 6000,
+					"rarity"	: 20
+				},
+				
+				"onSelectMessage" : 161,
+				"visitMode" : "once",
+				"selectMode" : "selectFirst",
+
+				"onVisited" : [
+					{
+						"message" : 163,
+						"bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ]
+					}
+				],
+				"rewards" : [
+					{
+						"appearChance" : { "max" : 30 },
+						"message" : 162,
+						"artifacts" : [ { "class" : "TREASURE" } ],
+						"bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ]
+					},
+					{
+						"appearChance" : { "min" : 30, "max" : 80 },
+						"message" : 162,
+						"artifacts" : [ { "class" : "MINOR" } ],
+						"bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ]
+					},
+					{
+						"appearChance" : { "min" : 80, "max" : 95 },
+						"message" : 162,
+						"artifacts" : [ { "class" : "MAJOR" } ],
+						"bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ]
+					},
+					{
+						"appearChance" : { "min" : 95 },
+						"message" : 162,
+						"artifacts" : [ { "class" : "RELIC" } ],
+						"bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ]
+					}
+				]
+			}
+		}
+	}
+}

+ 267 - 0
config/objects/rewardablePickable.json

@@ -0,0 +1,267 @@
+{
+	/// These are objects that covered by concept of "configurable object" and have their entire configuration in this config
+
+	"campfire" :{
+		"index" : 12,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"ambient" : ["LOOPCAMP"],
+				"visit" : ["EXPERNCE"],
+				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
+			}
+		},
+		"types" : {
+			"campfire" : {
+				"index" : 0,
+				"aiValue" : 2000,
+				"rmg" : {
+					"value"		: 2000,
+					"rarity"	: 500
+				},
+				
+				"blockedVisitable" : true,
+				"visitMode" : "unlimited",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 23,
+						"removeObject" : true,
+						"resources" : [
+							{
+								"list" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ],
+								"min" : 4,
+								"max" : 6
+							},
+							{
+								"type" : "gold",
+								"min" : 400,
+								"max" : 600
+							}
+						]
+					}
+				]
+			}
+		}
+	},
+	"flotsam" :{
+		"index" : 29,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["GENIE"],
+				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
+			}
+		},
+		"types" : {
+			"flotsam" : {
+				"index" : 0,
+				"aiValue" : 2000,
+				"rmg" : {
+					"value"		: 2000,
+					"rarity"	: 100
+				},
+				
+				"blockedVisitable" : true,
+				"visitMode" : "unlimited",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"message" : 51,
+						"appearChance" : { "max" : 25 },
+						"removeObject" : true,
+					},
+					{
+						"message" : 52,
+						"appearChance" : { "min" : 25, "max" : 50 },
+						"removeObject" : true,
+						"resources" : {
+							"wood" : 5
+						}
+					},
+					{
+						"message" : 53,
+						"appearChance" : { "min" : 50, "max" : 75 },
+						"removeObject" : true,
+						"resources" : {
+							"wood" : 5,
+							"gold" : 200
+						}
+					},
+					{
+						"message" : 54,
+						"appearChance" : { "min" : 75 },
+						"removeObject" : true,
+						"resources" : {
+							"wood" : 10,
+							"gold" : 500
+						}
+					}
+				]
+			}
+		}
+	},
+	"seaChest" :{
+		"index" : 82,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["CHEST"],
+				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
+			}
+		},
+		"types" : {
+			"seaChest" : {
+				"index" : 0,
+				"aiValue" : 1500,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 500
+				},
+				
+				"blockedVisitable" : true,
+				"visitMode" : "unlimited",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"appearChance" : { "max" : 20 },
+						"message" : 116,
+						"removeObject" : true,
+					},
+					{
+						"appearChance" : { "min" : 20, "max" : 30 },
+						"message" : 117,
+						"removeObject" : true,
+						"artifacts" : [
+							{ "class" : "TREASURE" }
+						],
+						"resources" : {
+							"gold" : 1000
+						}
+					},
+					{
+						"appearChance" : { "min" : 30 },
+						"message" : 118,
+						"removeObject" : true,
+						"resources" : {
+							"gold" : 1500
+						}
+					}
+				]
+			}
+		}
+	},
+	"shipwreckSurvivor" : {
+		"index" : 86,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["TREASURE"],
+				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
+			}
+		},
+		"types" : {
+			"shipwreckSurvivor" : {
+				"index" : 0,
+				"aiValue" : 1500,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 50
+				},
+
+				"blockedVisitable" : true,
+				"visitMode" : "unlimited",
+				"selectMode" : "selectFirst",
+				"rewards" : [
+					{
+						"appearChance" : { "max" : 55 },
+						"message" : 125,
+						"removeObject" : true,
+						"artifacts" : [ { "class" : "TREASURE" } ]
+					},
+					{
+						"appearChance" : { "min" : 55, "max" : 75 },
+						"message" : 125,
+						"removeObject" : true,
+						"artifacts" : [ { "class" : "MINOR" } ]
+					},
+					{
+						"appearChance" : { "min" : 75, "max" : 95 },
+						"message" : 125,
+						"removeObject" : true,
+						"artifacts" : [ { "class" : "MAJOR" } ]
+					},
+					{
+						"appearChance" : { "min" : 95 },
+						"message" : 125,
+						"removeObject" : true,
+						"artifacts" : [ { "class" : "RELIC" } ]
+					}
+				]
+			}
+		}
+	},
+	"treasureChest" : {
+		"index" : 101,
+		"handler": "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["CHEST"],
+				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
+			}
+		},
+		"types" : {
+			"treasureChest" : {
+				"index" : 0,
+				"aiValue" : 2000,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 1000
+				},
+				
+				"blockedVisitable" : true,
+				"onSelectMessage" : 146,
+				"visitMode" : "unlimited",
+				"selectMode" : "selectPlayer",
+				"rewards" : [
+					{
+						"appearChance" : { "max" : 33 },
+						"resources" : { "gold" : 2000 },
+						"removeObject" : true,
+					},
+					{
+						"appearChance" : { "max" : 33 },
+						"gainedExp" : 1500,
+						"removeObject" : true,
+					},
+					{
+						"appearChance" : { "min" : 33, "max" : 65 },
+						"resources" : { "gold" : 1500 },
+						"removeObject" : true,
+					},
+					{
+						"appearChance" : { "min" : 33, "max" : 65 },
+						"gainedExp" : 1000,
+						"removeObject" : true,
+					},
+					{
+						"appearChance" : { "min" : 65, "max" : 95 },
+						"resources" : { "gold" : 1000 },
+						"removeObject" : true,
+					},
+					{
+						"appearChance" : { "min" : 65, "max" : 95 },
+						"gainedExp" : 500,
+						"removeObject" : true,
+					},
+					{
+						"appearChance" : { "min" : 95 },
+						"message" : 145,
+						"removeObject" : true,
+						"artifacts" : [ { "class" : "TREASURE" } ]
+					}
+				]
+			}
+		}
+	}
+}

+ 0 - 3
lib/CGameState.cpp

@@ -142,9 +142,6 @@ void MetaString::getLocalString(const std::pair<ui8,ui32> &txt, std::string &dst
 		case GENERAL_TXT:
 			dst = VLC->generaltexth->translate("core.genrltxt", ser);
 			break;
-		case XTRAINFO_TXT:
-			dst = VLC->generaltexth->translate("core.xtrainfo", ser);
-			break;
 		case RES_NAMES:
 			dst = VLC->generaltexth->translate("core.restypes", ser);
 			break;

+ 0 - 2
lib/CGeneralTextHandler.cpp

@@ -409,7 +409,6 @@ CGeneralTextHandler::CGeneralTextHandler():
 	hcommands        (*this, "core.hallinfo" ),
 	fcommands        (*this, "core.castinfo" ),
 	advobtxt         (*this, "core.advevent" ),
-	xtrainfo         (*this, "core.xtrainfo" ),
 	restypes         (*this, "core.restypes" ),
 	randsign         (*this, "core.randsign" ),
 	overview         (*this, "core.overview" ),
@@ -439,7 +438,6 @@ CGeneralTextHandler::CGeneralTextHandler():
 	readToVector("core.hallinfo", "DATA/HALLINFO.TXT" );
 	readToVector("core.castinfo", "DATA/CASTINFO.TXT" );
 	readToVector("core.advevent", "DATA/ADVEVENT.TXT" );
-	readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" );
 	readToVector("core.restypes", "DATA/RESTYPES.TXT" );
 	readToVector("core.randsign", "DATA/RANDSIGN.TXT" );
 	readToVector("core.crgen1",   "DATA/CRGEN1.TXT"   );

+ 0 - 1
lib/CGeneralTextHandler.h

@@ -212,7 +212,6 @@ public:
 
 	//objects
 	LegacyTextContainer advobtxt;
-	LegacyTextContainer xtrainfo;
 	LegacyTextContainer restypes; //names of resources
 	LegacyTextContainer randsign;
 	LegacyTextContainer seerEmpty;

+ 21 - 1
lib/JsonNode.cpp

@@ -260,6 +260,21 @@ bool JsonNode::isNumber() const
 	return type == JsonType::DATA_INTEGER || type == JsonType::DATA_FLOAT;
 }
 
+bool JsonNode::isString() const
+{
+	return type == JsonType::DATA_STRING;
+}
+
+bool JsonNode::isVector() const
+{
+	return type == JsonType::DATA_VECTOR;
+}
+
+bool JsonNode::isStruct() const
+{
+	return type == JsonType::DATA_STRUCT;
+}
+
 bool JsonNode::containsBaseData() const
 {
 	switch(type)
@@ -796,7 +811,12 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
 	b->sid = static_cast<si32>(ability["sourceID"].Float());
 
 	if(!ability["description"].isNull())
-		b->description = ability["description"].String();
+	{
+		if (ability["description"].isString())
+			b->description = ability["description"].String();
+		if (ability["description"].isNumber())
+			b->description = VLC->generaltexth->translate("core.arraytxt", ability["description"].Integer());
+	}
 
 	value = &ability["effectRange"];
 	if (!value->isNull())

+ 3 - 0
lib/JsonNode.h

@@ -83,6 +83,9 @@ public:
 
 	bool isNull() const;
 	bool isNumber() const;
+	bool isString() const;
+	bool isVector() const;
+	bool isStruct() const;
 	/// true if node contains not-null data that cannot be extended via merging
 	/// used for generating common base node from multiple nodes (e.g. bonuses)
 	bool containsBaseData() const;

+ 1 - 1
lib/NetPacks.h

@@ -1178,7 +1178,7 @@ namespace ObjProperty
 		BANK_DAYCOUNTER, BANK_RESET, BANK_CLEAR,
 
 		//object with reward
-		REWARD_RESET, REWARD_SELECT
+		REWARD_RANDOMIZE, REWARD_SELECT, REWARD_CLEARED
 	};
 }
 

+ 1 - 1
lib/NetPacksBase.h

@@ -96,7 +96,7 @@ struct DLL_LINKAGE MetaString
 private:
 	enum EMessage {TEXACT_STRING, TLOCAL_STRING, TNUMBER, TREPLACE_ESTRING, TREPLACE_LSTRING, TREPLACE_NUMBER, TREPLACE_PLUSNUMBER};
 public:
-	enum {GENERAL_TXT=1, XTRAINFO_TXT, OBJ_NAMES, RES_NAMES, ART_NAMES, ARRAY_TXT, CRE_PL_NAMES, CREGENS, MINE_NAMES,
+	enum {GENERAL_TXT=1, OBJ_NAMES, RES_NAMES, ART_NAMES, ARRAY_TXT, CRE_PL_NAMES, CREGENS, MINE_NAMES,
 		MINE_EVNTS, ADVOB_TXT, ART_EVNTS, SPELL_NAME, SEC_SKILL_NAME, CRE_SING_NAMES, CREGENS4, COLOR, ART_DESCR, JK_TXT};
 
 	std::vector<ui8> message; //vector of EMessage

+ 0 - 7
lib/mapObjects/CObjectClassesHandler.cpp

@@ -54,7 +54,6 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER("artifact", CGArtifact);
 	SET_HANDLER("blackMarket", CGBlackMarket);
 	SET_HANDLER("boat", CGBoat);
-	SET_HANDLER("bonusingObject", CGBonusingObject);
 	SET_HANDLER("borderGate", CGBorderGate);
 	SET_HANDLER("borderGuard", CGBorderGuard);
 	SET_HANDLER("monster", CGCreature);
@@ -65,15 +64,11 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER("keymaster", CGKeymasterTent);
 	SET_HANDLER("lighthouse", CGLighthouse);
 	SET_HANDLER("magi", CGMagi);
-	SET_HANDLER("magicSpring", CGMagicSpring);
-	SET_HANDLER("magicWell", CGMagicWell);
 	SET_HANDLER("market", CGMarket);
 	SET_HANDLER("mine", CGMine);
 	SET_HANDLER("obelisk", CGObelisk);
 	SET_HANDLER("observatory", CGObservatory);
-	SET_HANDLER("onceVisitable", CGOnceVisitable);
 	SET_HANDLER("pandora", CGPandoraBox);
-	SET_HANDLER("pickable", CGPickable);
 	SET_HANDLER("prison", CGHeroInstance);
 	SET_HANDLER("questGuard", CGQuestGuard);
 	SET_HANDLER("resource", CGResource);
@@ -87,8 +82,6 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER("subterraneanGate", CGSubterraneanGate);
 	SET_HANDLER("whirlpool", CGWhirlpool);
 	SET_HANDLER("university", CGUniversity);
-	SET_HANDLER("oncePerHero", CGVisitableOPH);
-	SET_HANDLER("oncePerWeek", CGVisitableOPW);
 	SET_HANDLER("witch", CGWitchHut);
 	SET_HANDLER("terrain", CGTerrainPatch);
 

+ 149 - 43
lib/mapObjects/CRewardableConstructor.cpp

@@ -13,6 +13,7 @@
 #include "../CRandomGenerator.h"
 #include "../StringConstants.h"
 #include "../CCreatureHandler.h"
+#include "../CModHandler.h"
 #include "JsonRandom.h"
 #include "../IGameCallback.h"
 
@@ -45,11 +46,118 @@ void CRandomRewardObjectInfo::init(const JsonNode & objectConfig)
 	parameters = objectConfig;
 }
 
-void CRandomRewardObjectInfo::configureObject(CRewardableObject * object, CRandomGenerator & rng) const
+TRewardLimitersList CRandomRewardObjectInfo::configureSublimiters(CRewardableObject * object, CRandomGenerator & rng, const JsonNode & source) const
 {
-	std::map<si32, si32> thrownDice;
+	TRewardLimitersList result;
+	for (const auto & input : source.Vector())
+	{
+		auto newLimiter = std::make_shared<CRewardLimiter>();
+
+		configureLimiter(object, rng, *newLimiter, input);
+
+		result.push_back(newLimiter);
+	}
+
+	return result;
+}
+
+void CRandomRewardObjectInfo::configureLimiter(CRewardableObject * object, CRandomGenerator & rng, CRewardLimiter & limiter, const JsonNode & source) const
+{
+	std::vector<SpellID> spells;
+	for (size_t i=0; i<6; i++)
+		IObjectInterface::cb->getAllowedSpells(spells, static_cast<ui16>(i));
+
+
+	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.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng);
+	limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng);
+
+	limiter.resources = JsonRandom::loadResources(source["resources"], rng);
 
-	for (const JsonNode & reward : parameters["rewards"].Vector())
+	limiter.primary = JsonRandom::loadPrimary(source["primary"], rng);
+	limiter.secondary = JsonRandom::loadSecondary(source["secondary"], rng);
+	limiter.artifacts = JsonRandom::loadArtifacts(source["spells"], rng);
+	limiter.spells  = JsonRandom::loadSpells(source["artifacts"], rng, spells);
+	limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng);
+
+	limiter.allOf  = configureSublimiters(object, rng, source["allOf"] );
+	limiter.anyOf  = configureSublimiters(object, rng, source["anyOf"] );
+	limiter.noneOf = configureSublimiters(object, rng, source["noneOf"] );
+}
+
+void CRandomRewardObjectInfo::configureReward(CRewardableObject * object, CRandomGenerator & rng, CRewardInfo & reward, const JsonNode & source) const
+{
+	reward.resources = JsonRandom::loadResources(source["resources"], rng);
+
+	reward.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng)
+						  + JsonRandom::loadValue(source["gainedExp"], rng); // VCMI 1.1 compatibilty
+
+	reward.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng)
+						+ JsonRandom::loadValue(source["gainedLevels"], rng); // VCMI 1.1 compatibilty
+
+	reward.manaDiff = JsonRandom::loadValue(source["manaPoints"], rng);
+	reward.manaOverflowFactor = JsonRandom::loadValue(source["manaOverflowFactor"], rng);
+	reward.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, -1);
+
+	reward.movePoints = JsonRandom::loadValue(source["movePoints"], rng);
+	reward.movePercentage = JsonRandom::loadValue(source["movePercentage"], rng, -1);
+
+	reward.removeObject = source["removeObject"].Bool();
+	reward.bonuses = JsonRandom::loadBonuses(source["bonuses"]);
+
+	for (auto & bonus : reward.bonuses)
+	{
+		bonus.source = Bonus::OBJECT;
+		bonus.sid = object->ID;
+		//TODO: bonus.description = object->getObjectName();
+		if (bonus.type == Bonus::MORALE)
+			reward.extraComponents.push_back(Component(Component::MORALE, 0, bonus.val, 0));
+		if (bonus.type == Bonus::LUCK)
+			reward.extraComponents.push_back(Component(Component::LUCK, 0, bonus.val, 0));
+	}
+
+	reward.primary = JsonRandom::loadPrimary(source["primary"], rng);
+	reward.secondary = JsonRandom::loadSecondary(source["secondary"], rng);
+
+	std::vector<SpellID> spells;
+	for (size_t i=0; i<6; i++)
+		IObjectInterface::cb->getAllowedSpells(spells, static_cast<ui16>(i));
+
+	reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng);
+	reward.spells = JsonRandom::loadSpells(source["spells"], rng, spells);
+	reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng);
+
+	for ( auto node : source["changeCreatures"].Struct() )
+	{
+		CreatureID from (VLC->modh->identifiers.getIdentifier (node.second.meta, "creature", node.first) .get());
+		CreatureID dest (VLC->modh->identifiers.getIdentifier (node.second.meta, "creature", node.second.String()).get());
+
+		reward.extraComponents.push_back(Component(Component::CREATURE, dest.getNum(), 0, 0));
+
+		reward.creaturesChange[from] = dest;
+	}
+}
+
+void CRandomRewardObjectInfo::configureResetInfo(CRewardableObject * object, CRandomGenerator & rng, CRewardResetInfo & resetParameters, const JsonNode & source) const
+{
+	resetParameters.period   = static_cast<ui32>(source["period"].Float());
+	resetParameters.visitors = source["visitors"].Bool();
+	resetParameters.rewards  = source["rewards"].Bool();
+}
+
+void CRandomRewardObjectInfo::configureRewards(
+		CRewardableObject * object,
+		CRandomGenerator & rng, const
+		JsonNode & source,
+		std::map<si32, si32> & thrownDice,
+		CRewardVisitInfo::ERewardEventType event ) const
+{
+	for (const JsonNode & reward : source.Vector())
 	{
 		if (!reward["appearChance"].isNull())
 		{
@@ -57,7 +165,7 @@ void CRandomRewardObjectInfo::configureObject(CRewardableObject * object, CRando
 			si32 diceID = static_cast<si32>(chance["dice"].Float());
 
 			if (thrownDice.count(diceID) == 0)
-				thrownDice[diceID] = rng.getIntRange(1, 100)();
+				thrownDice[diceID] = rng.getIntRange(0, 99)();
 
 			if (!chance["min"].isNull())
 			{
@@ -68,61 +176,59 @@ void CRandomRewardObjectInfo::configureObject(CRewardableObject * object, CRando
 			if (!chance["max"].isNull())
 			{
 				int max = static_cast<int>(chance["max"].Float());
-				if (max < thrownDice[diceID])
+				if (max <= thrownDice[diceID])
 					continue;
 			}
 		}
 
-		const JsonNode & limiter = reward["limiter"];
-		CVisitInfo info;
-		// load limiter
-		info.limiter.numOfGrants = JsonRandom::loadValue(limiter["numOfGrants"], rng);
-		info.limiter.dayOfWeek = JsonRandom::loadValue(limiter["dayOfWeek"], rng);
-		info.limiter.minLevel = JsonRandom::loadValue(limiter["minLevel"], rng);
-		info.limiter.resources = JsonRandom::loadResources(limiter["resources"], rng);
+		CRewardVisitInfo info;
+		configureLimiter(object, rng, info.limiter, reward["limiter"]);
+		configureReward(object, rng, info.reward, reward);
 
-		info.limiter.primary = JsonRandom::loadPrimary(limiter["primary"], rng);
-		info.limiter.secondary = JsonRandom::loadSecondary(limiter["secondary"], rng);
-		info.limiter.artifacts = JsonRandom::loadArtifacts(limiter["artifacts"], rng);
-		info.limiter.creatures = JsonRandom::loadCreatures(limiter["creatures"], rng);
+		info.visitType = event;
+		info.message = loadMessage(reward["message"]);
 
-		info.reward.resources = JsonRandom::loadResources(reward["resources"], rng);
+		for (const auto & artifact : info.reward.artifacts )
+			info.message.addReplacement(MetaString::ART_NAMES, artifact.getNum());
 
-		info.reward.gainedExp = JsonRandom::loadValue(reward["gainedExp"], rng);
-		info.reward.gainedLevels = JsonRandom::loadValue(reward["gainedLevels"], rng);
+		for (const auto & artifact : info.reward.spells )
+			info.message.addReplacement(MetaString::SPELL_NAME, artifact.getNum());
 
-		info.reward.manaDiff = JsonRandom::loadValue(reward["manaPoints"], rng);
-		info.reward.manaPercentage = JsonRandom::loadValue(reward["manaPercentage"], rng, -1);
+		object->info.push_back(info);
+	}
+}
 
-		info.reward.movePoints = JsonRandom::loadValue(reward["movePoints"], rng);
-		info.reward.movePercentage = JsonRandom::loadValue(reward["movePercentage"], rng, -1);
-		
-		info.reward.removeObject = reward["removeObject"].Bool();
+void CRandomRewardObjectInfo::configureObject(CRewardableObject * object, CRandomGenerator & rng) const
+{
+	object->info.clear();
 
-		//FIXME: compile this line on Visual
-		//info.reward.bonuses = JsonRandom::loadBonuses(reward["bonuses"]);
+	std::map<si32, si32> thrownDice;
 
-		info.reward.primary = JsonRandom::loadPrimary(reward["primary"], rng);
-		info.reward.secondary = JsonRandom::loadSecondary(reward["secondary"], rng);
+	configureRewards(object, rng, parameters["rewards"], thrownDice, CRewardVisitInfo::EVENT_FIRST_VISIT);
+	configureRewards(object, rng, parameters["onVisited"], thrownDice, CRewardVisitInfo::EVENT_ALREADY_VISITED);
+	configureRewards(object, rng, parameters["onEmpty"], thrownDice, CRewardVisitInfo::EVENT_NOT_AVAILABLE);
 
-		std::vector<SpellID> spells;
-		for (size_t i=0; i<6; i++)
-			IObjectInterface::cb->getAllowedSpells(spells, static_cast<ui16>(i));
+	object->blockVisit= parameters["blockedVisitable"].Bool();
+	object->onSelect  = loadMessage(parameters["onSelectMessage"]);
 
-		info.reward.artifacts = JsonRandom::loadArtifacts(reward["artifacts"], rng);
-		info.reward.spells = JsonRandom::loadSpells(reward["spells"], rng, spells);
-		info.reward.creatures = JsonRandom::loadCreatures(reward["creatures"], rng);
+	if (!parameters["onVisitedMessage"].isNull())
+	{
+		CRewardVisitInfo onVisited;
+		onVisited.visitType = CRewardVisitInfo::EVENT_ALREADY_VISITED;
+		onVisited.message = loadMessage(parameters["onVisitedMessage"]);
+		object->info.push_back(onVisited);
+	}
 
-		info.message = loadMessage(reward["message"]);
-		info.selectChance = JsonRandom::loadValue(reward["selectChance"], rng);
-		
-		object->info.push_back(info);
+	if (!parameters["onEmptyMessage"].isNull())
+	{
+		CRewardVisitInfo onEmpty;
+		onEmpty.visitType = CRewardVisitInfo::EVENT_NOT_AVAILABLE;
+		onEmpty.message = loadMessage(parameters["onEmptyMessage"]);
+		object->info.push_back(onEmpty);
 	}
 
-	object->onSelect  = loadMessage(parameters["onSelectMessage"]);
-	object->onVisited = loadMessage(parameters["onVisitedMessage"]);
-	object->onEmpty   = loadMessage(parameters["onEmptyMessage"]);
-	object->resetDuration = static_cast<ui16>(parameters["resetDuration"].Float());
+	configureResetInfo(object, rng, object->resetParameters, parameters["resetParameters"]);
+
 	object->canRefuse = parameters["canRefuse"].Bool();
 	
 	auto visitMode = parameters["visitMode"].String();

+ 8 - 0
lib/mapObjects/CRewardableConstructor.h

@@ -19,6 +19,14 @@ VCMI_LIB_NAMESPACE_BEGIN
 class DLL_LINKAGE CRandomRewardObjectInfo : public IObjectInfo
 {
 	JsonNode parameters;
+
+	void configureRewards(CRewardableObject * object, CRandomGenerator & rng, const JsonNode & source, std::map<si32, si32> & thrownDice, CRewardVisitInfo::ERewardEventType mode) const;
+
+	void configureLimiter(CRewardableObject * object, CRandomGenerator & rng, CRewardLimiter & limiter, const JsonNode & source) const;
+	TRewardLimitersList configureSublimiters(CRewardableObject * object, CRandomGenerator & rng, const JsonNode & source) const;
+
+	void configureReward(CRewardableObject * object, CRandomGenerator & rng, CRewardInfo & info, const JsonNode & source) const;
+	void configureResetInfo(CRewardableObject * object, CRandomGenerator & rng, CRewardResetInfo & info, const JsonNode & source) const;
 public:
 	bool givesResources() const override;
 

+ 176 - 789
lib/mapObjects/CRewardableObject.cpp

@@ -30,6 +30,12 @@ bool CRewardLimiter::heroAllowed(const CGHeroInstance * hero) const
 			return false;
 	}
 
+	if(daysPassed != 0)
+	{
+		if (IObjectInterface::cb->getDate(Date::DAY) < daysPassed)
+			return false;
+	}
+
 	for(auto & reqStack : creatures)
 	{
 		size_t count = 0;
@@ -46,7 +52,16 @@ bool CRewardLimiter::heroAllowed(const CGHeroInstance * hero) const
 	if(!IObjectInterface::cb->getPlayerState(hero->tempOwner)->resources.canAfford(resources))
 		return false;
 
-	if(minLevel > (si32)hero->level)
+	if(heroLevel > (si32)hero->level)
+		return false;
+
+	if((TExpType)heroExperience > hero->exp)
+		return false;
+
+	if(manaPoints > hero->mana)
+		return false;
+
+	if(manaPercentage > 100 * hero->mana / hero->manaLimit())
 		return false;
 
 	for(size_t i=0; i<primary.size(); i++)
@@ -61,25 +76,50 @@ bool CRewardLimiter::heroAllowed(const CGHeroInstance * hero) const
 			return false;
 	}
 
+	for(auto & spell : spells)
+	{
+		if (!hero->spellbookContainsSpell(spell))
+			return false;
+	}
+
 	for(auto & art : artifacts)
 	{
 		if (!hero->hasArt(art))
 			return false;
 	}
 
-	return true;
+	for(auto & sublimiter : noneOf)
+	{
+		if (sublimiter->heroAllowed(hero))
+			return false;
+	}
+
+	for(auto & sublimiter : allOf)
+	{
+		if (!sublimiter->heroAllowed(hero))
+			return false;
+	}
+
+	if ( anyOf.size() == 0 )
+		return true;
+
+	for(auto & sublimiter : anyOf)
+	{
+		if (sublimiter->heroAllowed(hero))
+			return true;
+	}
+	return false;
 }
 
-std::vector<ui32> CRewardableObject::getAvailableRewards(const CGHeroInstance * hero) const
+std::vector<ui32> CRewardableObject::getAvailableRewards(const CGHeroInstance * hero, CRewardVisitInfo::ERewardEventType event) const
 {
 	std::vector<ui32> ret;
 
 	for(size_t i=0; i<info.size(); i++)
 	{
-		const CVisitInfo & visit = info[i];
+		const CRewardVisitInfo & visit = info[i];
 
-		if((visit.limiter.numOfGrants == 0 || visit.numOfGrants < visit.limiter.numOfGrants) // reward has unlimited uses or some are still available
-			&& visit.limiter.heroAllowed(hero))
+		if(event == visit.visitType && visit.limiter.heroAllowed(hero))
 		{
 			logGlobal->trace("Reward %d is allowed", i);
 			ret.push_back(static_cast<ui32>(i));
@@ -88,16 +128,11 @@ std::vector<ui32> CRewardableObject::getAvailableRewards(const CGHeroInstance *
 	return ret;
 }
 
-CVisitInfo CRewardableObject::getVisitInfo(int index, const CGHeroInstance *) const
-{
-	return info[index];
-}
-
 void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
 {
-	auto grantRewardWithMessage = [&](int index) -> void
+	auto grantRewardWithMessage = [&](int index, bool markAsVisit) -> void
 	{
-		auto vi = getVisitInfo(index, h);
+		auto vi = info[index];
 		logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString());
 		// show message only if it is not empty
 		if (!vi.message.toString().empty())
@@ -109,68 +144,63 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
 			cb->showInfoDialog(&iw);
 		}
 		// grant reward afterwards. Note that it may remove object
-		grantReward(index, h);
+		grantReward(index, h, markAsVisit);
 	};
-	auto selectRewardsMessage = [&](std::vector<ui32> rewards) -> void
+	auto selectRewardsMessage = [&](std::vector<ui32> rewards, const MetaString & dialog) -> void
 	{
 		BlockingDialog sd(canRefuse, rewards.size() > 1);
 		sd.player = h->tempOwner;
-		sd.text = onSelect;
+		sd.text = dialog;
 		for (auto index : rewards)
-			sd.components.push_back(getVisitInfo(index, h).reward.getDisplayedComponent(h));
+			sd.components.push_back(info[index].reward.getDisplayedComponent(h));
 		cb->showBlockingDialog(&sd);
 	};
 
-	if(!wasVisited(h))
+	if(!wasVisitedBefore(h))
 	{
-		auto rewards = getAvailableRewards(h);
+		auto rewards = getAvailableRewards(h, CRewardVisitInfo::EVENT_FIRST_VISIT);
 		bool objectRemovalPossible = false;
 		for(auto index : rewards)
 		{
-			if(getVisitInfo(index, h).reward.removeObject)
+			if(info[index].reward.removeObject)
 				objectRemovalPossible = true;
 		}
 
 		logGlobal->debug("Visiting object with %d possible rewards", rewards.size());
 		switch (rewards.size())
 		{
-			case 0: // no available rewards, e.g. empty flotsam
+			case 0: // no available rewards, e.g. visiting School of War without gold
 			{
-				InfoWindow iw;
-				iw.player = h->tempOwner;
-				if (!onEmpty.toString().empty())
-					iw.text = onEmpty;
+				auto emptyRewards = getAvailableRewards(h, CRewardVisitInfo::EVENT_NOT_AVAILABLE);
+				if (!emptyRewards.empty())
+					grantRewardWithMessage(emptyRewards[0], false);
 				else
-					iw.text = onVisited;
-				cb->showInfoDialog(&iw);
+					logMod->warn("No applicable message for visiting empty object!");
 				break;
 			}
 			case 1: // one reward. Just give it with message
 			{
 				if (canRefuse)
-					selectRewardsMessage(rewards);
+					selectRewardsMessage(rewards, info[rewards[0]].message);
 				else
-					grantRewardWithMessage(rewards[0]);
+					grantRewardWithMessage(rewards[0], true);
 				break;
 			}
 			default: // multiple rewards. Act according to select mode
 			{
 				switch (selectMode) {
 					case SELECT_PLAYER: // player must select
-						selectRewardsMessage(rewards);
+						selectRewardsMessage(rewards, onSelect);
 						break;
 					case SELECT_FIRST: // give first available
-						grantRewardWithMessage(rewards[0]);
-						break;
-					case SELECT_RANDOM: // select one randomly //TODO: use weights
-						grantRewardWithMessage(rewards[CRandomGenerator::getDefault().nextInt((int)rewards.size()-1)]);
+						grantRewardWithMessage(rewards[0], true);
 						break;
 				}
 				break;
 			}
 		}
 
-		if(!objectRemovalPossible && getAvailableRewards(h).size() == 0)
+		if(!objectRemovalPossible && getAvailableRewards(h, CRewardVisitInfo::EVENT_FIRST_VISIT).size() == 0)
 		{
 			ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id);
 			cb->sendAndApply(&cov);
@@ -179,19 +209,18 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
 	else
 	{
 		logGlobal->debug("Revisiting already visited object");
-		InfoWindow iw;
-		iw.player = h->tempOwner;
-		if (!onVisited.toString().empty())
-			iw.text = onVisited;
+
+		auto visitedRewards = getAvailableRewards(h, CRewardVisitInfo::EVENT_ALREADY_VISITED);
+		if (!visitedRewards.empty())
+			grantRewardWithMessage(visitedRewards[0], false);
 		else
-			iw.text = onEmpty;
-		cb->showInfoDialog(&iw);
+			logMod->warn("No applicable message for visiting already visited object!");
 	}
 }
 
 void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const
 {
-	grantRewardAfterLevelup(getVisitInfo(selectedReward, hero), hero);
+	grantRewardAfterLevelup(info[selectedReward], hero);
 }
 
 void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
@@ -201,8 +230,8 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32
 
 	if(answer > 0 && answer-1 < info.size())
 	{
-		auto list = getAvailableRewards(hero);
-		grantReward(list[answer - 1], hero);
+		auto list = getAvailableRewards(hero, CRewardVisitInfo::EVENT_FIRST_VISIT);
+		grantReward(list[answer - 1], hero, true);
 	}
 	else
 	{
@@ -210,21 +239,21 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32
 	}
 }
 
-void CRewardableObject::onRewardGiven(const CGHeroInstance * hero) const
+void CRewardableObject::grantReward(ui32 rewardID, const CGHeroInstance * hero, bool markVisited) const
 {
-	// no implementation, virtual function for overrides
-}
+	if (markVisited)
+	{
+		cb->setObjProperty(id, ObjProperty::REWARD_CLEARED, true);
 
-void CRewardableObject::grantReward(ui32 rewardID, const CGHeroInstance * hero) const
-{
-	ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id);
-	cb->sendAndApply(&cov);
-	cb->setObjProperty(id, ObjProperty::REWARD_SELECT, rewardID);
+		ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id);
+		cb->sendAndApply(&cov);
+	}
 
-	grantRewardBeforeLevelup(getVisitInfo(rewardID, hero), hero);
+	cb->setObjProperty(id, ObjProperty::REWARD_SELECT, rewardID);
+	grantRewardBeforeLevelup(info[rewardID], hero);
 }
 
-void CRewardableObject::grantRewardBeforeLevelup(const CVisitInfo & info, const CGHeroInstance * hero) const
+void CRewardableObject::grantRewardBeforeLevelup(const CRewardVisitInfo & info, const CGHeroInstance * hero) const
 {
 	assert(hero);
 	assert(hero->tempOwner.isValidPlayer());
@@ -245,12 +274,16 @@ void CRewardableObject::grantRewardBeforeLevelup(const CVisitInfo & info, const
 	}
 
 	for(int i=0; i< info.reward.primary.size(); i++)
-		if(info.reward.primary[i] > 0)
-			cb->changePrimSkill(hero, static_cast<PrimarySkill::PrimarySkill>(i), info.reward.primary[i], false);
+		cb->changePrimSkill(hero, static_cast<PrimarySkill::PrimarySkill>(i), info.reward.primary[i], false);
 
 	si64 expToGive = 0;
-	expToGive += VLC->heroh->reqExp(hero->level+info.reward.gainedLevels) - VLC->heroh->reqExp(hero->level);
-	expToGive += hero->calculateXp(info.reward.gainedExp);
+
+	if (info.reward.heroLevel > 0)
+		expToGive += VLC->heroh->reqExp(hero->level+info.reward.heroLevel) - VLC->heroh->reqExp(hero->level);
+
+	if (info.reward.heroExperience > 0)
+		expToGive += hero->calculateXp(info.reward.heroExperience);
+
 	if(expToGive)
 		cb->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expToGive);
 
@@ -261,15 +294,21 @@ void CRewardableObject::grantRewardBeforeLevelup(const CVisitInfo & info, const
 	}
 }
 
-void CRewardableObject::grantRewardAfterLevelup(const CVisitInfo & info, const CGHeroInstance * hero) const
+void CRewardableObject::grantRewardAfterLevelup(const CRewardVisitInfo & info, const CGHeroInstance * hero) const
 {
 	if(info.reward.manaDiff || info.reward.manaPercentage >= 0)
 	{
-		si32 mana = hero->mana;
+		si32 manaScaled = hero->mana;
 		if (info.reward.manaPercentage >= 0)
-			mana = hero->manaLimit() * info.reward.manaPercentage / 100;
+			manaScaled = hero->manaLimit() * info.reward.manaPercentage / 100;
+
+		si32 manaMissing   = std::max(0, hero->manaLimit() - manaScaled);
+		si32 manaGranted   = std::min(manaMissing, info.reward.manaDiff);
+		si32 manaOverflow  = info.reward.manaDiff - manaGranted;
+		si32 manaOverLimit = manaOverflow * info.reward.manaOverflowFactor / 100;
+		si32 manaOutput    = manaScaled + manaGranted + manaOverLimit;
 
-		cb->setManaPoints(hero->id, mana + info.reward.manaDiff);
+		cb->setManaPoints(hero->id, manaOutput);
 	}
 
 	if(info.reward.movePoints || info.reward.movePercentage >= 0)
@@ -305,6 +344,24 @@ void CRewardableObject::grantRewardAfterLevelup(const CVisitInfo & info, const C
 		cb->changeSpells(hero, true, spellsToGive);
 	}
 
+	if(!info.reward.creaturesChange.empty())
+	{
+		for (auto slot : hero->Slots())
+		{
+			const CStackInstance * heroStack = slot.second;
+
+			for (auto & change : info.reward.creaturesChange)
+			{
+				if (heroStack->type->getId() == change.first)
+				{
+					StackLocation location(hero, slot.first);
+					cb->changeStackType(location, change.second.toCreature());
+					break;
+				}
+			}
+		}
+	}
+
 	if(!info.reward.creatures.empty())
 	{
 		CCreatureSet creatures;
@@ -314,23 +371,39 @@ void CRewardableObject::grantRewardAfterLevelup(const CVisitInfo & info, const C
 		cb->giveCreatures(this, hero, creatures, false);
 	}
 
-	onRewardGiven(hero);
-
 	if(info.reward.removeObject)
 		cb->removeObject(this);
 }
 
-bool CRewardableObject::wasVisited(PlayerColor player) const
+bool CRewardableObject::wasVisitedBefore(const CGHeroInstance * contextHero) const
 {
 	switch (visitMode)
 	{
 		case VISIT_UNLIMITED:
-		case VISIT_BONUS:
 			return false;
 		case VISIT_ONCE:
-			return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id));
+			return onceVisitableObjectCleared;
+		case VISIT_PLAYER:
+			return vstd::contains(cb->getPlayerState(contextHero->getOwner())->visitedObjects, ObjectInstanceID(id));
+		case VISIT_BONUS:
+			return contextHero->hasBonusFrom(Bonus::OBJECT, ID);
 		case VISIT_HERO:
+			return contextHero->visitedObjects.count(ObjectInstanceID(id));
+		default:
 			return false;
+	}
+
+}
+
+bool CRewardableObject::wasVisited(PlayerColor player) const
+{
+	switch (visitMode)
+	{
+		case VISIT_UNLIMITED:
+		case VISIT_BONUS:
+		case VISIT_HERO:
+			return false;
+		case VISIT_ONCE:
 		case VISIT_PLAYER:
 			return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id));
 		default:
@@ -342,8 +415,6 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const
 {
 	switch (visitMode)
 	{
-		case VISIT_UNLIMITED:
-			return false;
 		case VISIT_BONUS:
 			return h->hasBonusFrom(Bonus::OBJECT, ID);
 		case VISIT_HERO:
@@ -353,20 +424,32 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const
 	}
 }
 
+CRewardableObject::EVisitMode CRewardableObject::getVisitMode() const
+{
+	return EVisitMode(visitMode);
+}
+
+ui16 CRewardableObject::getResetDuration() const
+{
+	return resetParameters.period;
+}
+
 void CRewardInfo::loadComponents(std::vector<Component> & comps,
                                  const CGHeroInstance * h) const
 {
 	for (auto comp : extraComponents)
 		comps.push_back(comp);
 
-	if (gainedExp)
+	if (heroExperience)
 	{
 		comps.push_back(Component(
-			Component::EXPERIENCE, 0, (si32)h->calculateXp(gainedExp), 0));
+			Component::EXPERIENCE, 0, (si32)h->calculateXp(heroExperience), 0));
 	}
-	if (gainedLevels) comps.push_back(Component(Component::EXPERIENCE, 0, gainedLevels, 0));
+	if (heroLevel)
+		comps.push_back(Component(Component::EXPERIENCE, 1, heroLevel, 0));
 
-	if (manaDiff) comps.push_back(Component(Component::PRIM_SKILL, 5, manaDiff, 0));
+	if (manaDiff || manaPercentage >= 0)
+		comps.push_back(Component(Component::PRIM_SKILL, 5, manaDiff, 0));
 
 	for (size_t i=0; i<primary.size(); i++)
 	{
@@ -426,26 +509,37 @@ void CRewardableObject::setPropertyDer(ui8 what, ui32 val)
 {
 	switch (what)
 	{
-		case ObjProperty::REWARD_RESET:
-			for (auto & visit : info)
-				visit.numOfGrants = 0;
+		case ObjProperty::REWARD_RANDOMIZE:
+			initObj(cb->gameState()->getRandomGenerator());
 			break;
 		case ObjProperty::REWARD_SELECT:
 			selectedReward = val;
-			info[val].numOfGrants++;
+			break;
+		case ObjProperty::REWARD_CLEARED:
+			onceVisitableObjectCleared = val;
 			break;
 	}
 }
 
-void CRewardableObject::triggerRewardReset() const
+void CRewardableObject::triggerReset() const
 {
-	cb->setObjProperty(id, ObjProperty::REWARD_RESET, 0);
+	if (resetParameters.rewards)
+	{
+		cb->setObjProperty(id, ObjProperty::REWARD_RANDOMIZE, 0);
+	}
+	if (resetParameters.visitors)
+	{
+		cb->setObjProperty(id, ObjProperty::REWARD_CLEARED, false);
+
+		ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_CLEAR, id);
+		cb->sendAndApply(&cov);
+	}
 }
 
 void CRewardableObject::newTurn(CRandomGenerator & rand) const
 {
-	if (resetDuration != 0 && cb->getDate(Date::DAY) > 1 && (cb->getDate(Date::DAY) % resetDuration) == 1)
-		triggerRewardReset();
+	if (resetParameters.period != 0 && cb->getDate(Date::DAY) > 1 && (cb->getDate(Date::DAY) % resetParameters.period) == 1)
+		triggerReset();
 }
 
 void CRewardableObject::initObj(CRandomGenerator & rand)
@@ -457,715 +551,8 @@ CRewardableObject::CRewardableObject():
 	selectMode(0),
 	visitMode(0),
 	selectedReward(0),
-	resetDuration(0),
+	onceVisitableObjectCleared(false),
 	canRefuse(false)
 {}
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-///               END OF CODE FOR CREWARDABLEOBJECT AND RELATED CLASSES                         ///
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-/// Helper, selects random art class based on weights
-static int selectRandomArtClass(CRandomGenerator & rand, int treasure, int minor, int major, int relic)
-{
-	int total = treasure + minor + major + relic;
-	assert(total != 0);
-	int hlp = rand.nextInt(total - 1);
-
-	if(hlp < treasure)
-		return CArtifact::ART_TREASURE;
-	if(hlp < treasure + minor)
-		return CArtifact::ART_MINOR;
-	if(hlp < treasure + minor + major)
-		return CArtifact::ART_MAJOR;
-	return CArtifact::ART_RELIC;
-}
-
-/// Helper, adds random artifact to reward selecting class based on weights
-static void loadRandomArtifact(CRandomGenerator & rand, CVisitInfo & info, int treasure, int minor, int major, int relic)
-{
-	int artClass = selectRandomArtClass(rand, treasure, minor, major, relic);
-	ArtifactID artID = VLC->arth->pickRandomArtifact(rand, artClass);
-	info.reward.artifacts.push_back(artID);
-}
-
-CGPickable::CGPickable()
-{
-	visitMode = VISIT_UNLIMITED;
-	selectMode = SELECT_PLAYER;
-}
-
-void CGPickable::initObj(CRandomGenerator & rand)
-{
-	blockVisit = true;
-	switch(ID)
-	{
-	case Obj::CAMPFIRE:
-		{
-			int givenRes = rand.nextInt(5);
-			int givenAmm = rand.nextInt(4, 6);
-
-			info.resize(1);
-			info[0].reward.resources[givenRes] = givenAmm;
-			info[0].reward.resources[Res::GOLD]= givenAmm * 100;
-			info[0].message.addTxt(MetaString::ADVOB_TXT,23);
-			info[0].reward.removeObject = true;
-			break;
-		}
-	case Obj::FLOTSAM:
-		{
-			int type = rand.nextInt(3);
-			switch(type)
-			{
-			case 0:
-					info.resize(1);
-					info[0].message.addTxt(MetaString::ADVOB_TXT, 51);
-					info[0].reward.removeObject = true;
-					break;
-			case 1:
-				{
-					info.resize(1);
-					info[0].reward.resources[Res::WOOD] = 5;
-					info[0].message.addTxt(MetaString::ADVOB_TXT, 52);
-					info[0].reward.removeObject = true;
-					break;
-				}
-			case 2:
-				{
-					info.resize(1);
-					info[0].reward.resources[Res::WOOD] = 5;
-					info[0].reward.resources[Res::GOLD] = 200;
-					info[0].message.addTxt(MetaString::ADVOB_TXT, 53);
-					info[0].reward.removeObject = true;
-					break;
-				}
-			case 3:
-				{
-					info.resize(1);
-					info[0].reward.resources[Res::WOOD] = 10;
-					info[0].reward.resources[Res::GOLD] = 500;
-					info[0].message.addTxt(MetaString::ADVOB_TXT, 54);
-					info[0].reward.removeObject = true;
-					break;
-				}
-			}
-			break;
-		}
-	case Obj::SEA_CHEST:
-		{
-			int hlp = rand.nextInt(99);
-			if(hlp < 20)
-			{
-				info.resize(1);
-				info[0].message.addTxt(MetaString::ADVOB_TXT, 116);
-				info[0].reward.removeObject = true;
-			}
-			else if(hlp < 90)
-			{
-				info.resize(1);
-				info[0].reward.resources[Res::GOLD] = 1500;
-				info[0].message.addTxt(MetaString::ADVOB_TXT, 118);
-				info[0].reward.removeObject = true;
-			}
-			else
-			{
-				info.resize(1);
-				loadRandomArtifact(rand, info[0], 100, 0, 0, 0);
-				info[0].reward.resources[Res::GOLD] = 1000;
-				info[0].message.addTxt(MetaString::ADVOB_TXT, 117);
-				info[0].message.addReplacement(MetaString::ART_NAMES, info[0].reward.artifacts.back());
-				info[0].reward.removeObject = true;
-			}
-		}
-		break;
-	case Obj::SHIPWRECK_SURVIVOR:
-		{
-			info.resize(1);
-			loadRandomArtifact(rand, info[0], 55, 20, 20, 5);
-			info[0].message.addTxt(MetaString::ADVOB_TXT, 125);
-			info[0].message.addReplacement(MetaString::ART_NAMES, info[0].reward.artifacts.back());
-			info[0].reward.removeObject = true;
-		}
-		break;
-	case Obj::TREASURE_CHEST:
-		{
-			int hlp = rand.nextInt(99);
-			if(hlp >= 95)
-			{
-				info.resize(1);
-				loadRandomArtifact(rand, info[0], 100, 0, 0, 0);
-				info[0].message.addTxt(MetaString::ADVOB_TXT,145);
-				info[0].message.addReplacement(MetaString::ART_NAMES, info[0].reward.artifacts.back());
-				info[0].reward.removeObject = true;
-				return;
-			}
-			else if (hlp >= 65)
-			{
-				onSelect.addTxt(MetaString::ADVOB_TXT,146);
-				info.resize(2);
-				info[0].reward.resources[Res::GOLD] = 2000;
-				info[1].reward.gainedExp = 1500;
-				info[0].reward.removeObject = true;
-				info[1].reward.removeObject = true;
-			}
-			else if(hlp >= 33)
-			{
-				onSelect.addTxt(MetaString::ADVOB_TXT,146);
-				info.resize(2);
-				info[0].reward.resources[Res::GOLD] = 1500;
-				info[1].reward.gainedExp = 1000;
-				info[0].reward.removeObject = true;
-				info[1].reward.removeObject = true;
-			}
-			else
-			{
-				onSelect.addTxt(MetaString::ADVOB_TXT,146);
-				info.resize(2);
-				info[0].reward.resources[Res::GOLD] = 1000;
-				info[1].reward.gainedExp = 500;
-				info[0].reward.removeObject = true;
-				info[1].reward.removeObject = true;
-			}
-		}
-		break;
-	}
-}
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-CGBonusingObject::CGBonusingObject()
-{
-	visitMode = VISIT_BONUS;
-	selectMode = SELECT_FIRST;
-}
-
-void CGBonusingObject::initObj(CRandomGenerator & rand)
-{
-	auto configureBonusDuration = [&](CVisitInfo & visit, Bonus::BonusDuration duration, Bonus::BonusType type, si32 value, si32 descrID)
-	{
-		Bonus b(duration, type, Bonus::OBJECT, value, ID, descrID != 0 ? VLC->generaltexth->advobtxt[descrID] : "");
-		visit.reward.bonuses.push_back(b);
-		if (type == Bonus::MORALE)
-			visit.reward.extraComponents.push_back(Component(Component::MORALE, 0, value, 0));
-		if (type == Bonus::LUCK)
-			visit.reward.extraComponents.push_back(Component(Component::LUCK, 0, value, 0));
-	};
-
-	auto configureBonus = [&](CVisitInfo & visit, Bonus::BonusType type, si32 value, si32 descrID)
-	{
-		configureBonusDuration(visit, Bonus::ONE_BATTLE, type, value, descrID);
-	};
-
-	auto configureMessage = [&](CVisitInfo & visit, int onGrantID, int onVisitedID)
-	{
-		visit.message.addTxt(MetaString::ADVOB_TXT, onGrantID);
-		onVisited.addTxt(MetaString::ADVOB_TXT, onVisitedID);
-	};
-
-	info.resize(1);
-
-	switch(ID)
-	{
-	case Obj::BUOY:
-			blockVisit = true;
-		configureMessage(info[0], 21, 22);
-		configureBonus(info[0], Bonus::MORALE, +1, 94);
-		break;
-	case Obj::SWAN_POND:
-		configureMessage(info[0], 29, 30);
-		configureBonus(info[0], Bonus::LUCK, 2, 67);
-		info[0].reward.movePercentage = 0;
-		break;
-	case Obj::FAERIE_RING:
-		configureMessage(info[0], 49, 50);
-		configureBonus(info[0], Bonus::LUCK, 1, 71);
-		break;
-	case Obj::FOUNTAIN_OF_FORTUNE:
-		selectMode = SELECT_RANDOM;
-		info.resize(5);
-		for (int i=0; i<5; i++)
-		{
-			configureBonus(info[i], Bonus::LUCK, i-1, 69); //NOTE: description have %d that should be replaced with value
-			info[i].message.addTxt(MetaString::ADVOB_TXT, 55);
-		}
-		onVisited.addTxt(MetaString::ADVOB_TXT, 56);
-		break;
-	case Obj::IDOL_OF_FORTUNE:
-
-		info.resize(7);
-		for (int i=0; i<6; i++)
-		{
-			info[i].limiter.dayOfWeek = i+1;
-			configureBonus(info[i], (i%2) ? Bonus::MORALE : Bonus::LUCK, 1, 68);
-			info[i].message.addTxt(MetaString::ADVOB_TXT, 62);
-		}
-		info.back().limiter.dayOfWeek = 7;
-		configureBonus(info.back(), Bonus::MORALE, 1, 68); // on last day of week
-		configureBonus(info.back(), Bonus::LUCK,   1, 68);
-		configureMessage(info.back(), 62, 63);
-
-		break;
-	case Obj::MERMAID:
-		blockVisit = true;
-		configureMessage(info[0], 83, 82);
-		configureBonus(info[0], Bonus::LUCK, 1, 72);
-		break;
-	case Obj::RALLY_FLAG:
-		configureMessage(info[0], 111, 110);
-		configureBonus(info[0], Bonus::MORALE, 1, 102);
-		configureBonus(info[0], Bonus::LUCK,   1, 102);
-		info[0].reward.movePoints = 400;
-		break;
-	case Obj::OASIS:
-		configureMessage(info[0], 95, 94);
-		configureBonus(info[0], Bonus::MORALE, 1, 95);
-		info[0].reward.movePoints = 800;
-		break;
-	case Obj::TEMPLE:
-		info[0].limiter.dayOfWeek = 7;
-		info.resize(2);
-		configureBonus(info[0], Bonus::MORALE, 2, 96);
-		configureBonus(info[1], Bonus::MORALE, 1, 97);
-
-		info[0].message.addTxt(MetaString::ADVOB_TXT, 140);
-		info[1].message.addTxt(MetaString::ADVOB_TXT, 140);
-		onVisited.addTxt(MetaString::ADVOB_TXT, 141);
-		break;
-	case Obj::WATERING_HOLE:
-		configureMessage(info[0], 166, 167);
-		configureBonus(info[0], Bonus::MORALE, 1, 100);
-		info[0].reward.movePoints = 400;
-		break;
-	case Obj::FOUNTAIN_OF_YOUTH:
-		configureMessage(info[0], 57, 58);
-		configureBonus(info[0], Bonus::MORALE, 1, 103);
-		info[0].reward.movePoints = 400;
-		break;
-	case Obj::STABLES:
-		configureMessage(info[0], 137, 136);
-		configureBonusDuration(info[0], Bonus::ONE_WEEK, Bonus::LAND_MOVEMENT, 400, 0);
-		info[0].reward.movePoints = 400;
-		break;
-	}
-}
-
-CVisitInfo CGBonusingObject::getVisitInfo(int index, const CGHeroInstance *h) const
-{
-	if(ID == Obj::STABLES)
-	{
-		assert(index == 0);
-		for(auto& slot : h->Slots())
-		{
-			if(slot.second->type->idNumber == CreatureID::CAVALIER)
-			{
-				CVisitInfo vi(info[0]);
-				vi.message.clear();
-				vi.message.addTxt(MetaString::ADVOB_TXT, 138);
-				vi.reward.extraComponents.push_back(Component(
-					Component::CREATURE, CreatureID::CHAMPION, 0, 1));
-				return vi;
-			}
-		}
-	}
-	return info[index];
-}
-
-void CGBonusingObject::onHeroVisit(const CGHeroInstance *h) const
-{
-	CRewardableObject::onHeroVisit(h);
-	if(ID == Obj::STABLES)
-	{
-		//regardless of whether this hero visited stables or not, cavaliers must be upgraded
-		for(auto& slot : h->Slots())
-		{
-			if(slot.second->type->idNumber == CreatureID::CAVALIER)
-			{
-				cb->changeStackType(StackLocation(h, slot.first),
-									VLC->creh->objects[CreatureID::CHAMPION]);
-			}
-		}
-	}
-}
-
-bool CGBonusingObject::wasVisited(const CGHeroInstance * h) const
-{
-	if(ID == Obj::STABLES)
-	{
-		for(auto& slot : h->Slots())
-		{
-			if(slot.second->type->idNumber == CreatureID::CAVALIER)
-			{
-				// always display the reward message if the hero got cavaliers
-				return false;
-			}
-		}
-	}
-	return CRewardableObject::wasVisited(h);
-}
-
-void CGBonusingObject::grantReward(ui32 rewardID, const CGHeroInstance * hero) const
-{
-	if(ID == Obj::STABLES && CRewardableObject::wasVisited(hero))
-	{
-		// reward message has been displayed - do not give the actual bonus
-		return;
-	}
-	CRewardableObject::grantReward(rewardID, hero);
-}
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-CGOnceVisitable::CGOnceVisitable()
-{
-	visitMode = VISIT_ONCE;
-	selectMode = SELECT_FIRST;
-}
-
-void CGOnceVisitable::initObj(CRandomGenerator & rand)
-{
-	switch(ID)
-	{
-	case Obj::CORPSE:
-		{
-			onEmpty.addTxt(MetaString::ADVOB_TXT, 38);
-			blockVisit = true;
-			if(rand.nextInt(99) < 20)
-			{
-				info.resize(1);
-				loadRandomArtifact(rand, info[0], 10, 10, 10, 0);
-				info[0].message.addTxt(MetaString::ADVOB_TXT, 37);
-				info[0].limiter.numOfGrants = 1;
-			}
-		}
-		break;
-	case Obj::LEAN_TO:
-		{
-			onEmpty.addTxt(MetaString::ADVOB_TXT, 65);
-			info.resize(1);
-			int type =  rand.nextInt(5); //any basic resource without gold
-			int value = rand.nextInt(1, 4);
-			info[0].reward.resources[type] = value;
-			info[0].message.addTxt(MetaString::ADVOB_TXT, 64);
-			info[0].limiter.numOfGrants = 1;
-		}
-		break;
-	case Obj::WARRIORS_TOMB:
-		{
-			onSelect.addTxt(MetaString::ADVOB_TXT, 161);
-			onVisited.addTxt(MetaString::ADVOB_TXT, 163);
-
-			info.resize(1);
-			loadRandomArtifact(rand, info[0], 30, 50, 25, 5);
-
-			Bonus bonus(Bonus::ONE_BATTLE, Bonus::MORALE, Bonus::OBJECT, -3, ID);
-			info[0].reward.bonuses.push_back(bonus);
-			info[0].limiter.numOfGrants = 1;
-			info[0].message.addTxt(MetaString::ADVOB_TXT, 162);
-			info[0].message.addReplacement(VLC->arth->objects[info[0].reward.artifacts.back()]->getNameTranslated());
-		}
-		break;
-	case Obj::WAGON:
-		{
-			onVisited.addTxt(MetaString::ADVOB_TXT, 156);
-
-			int hlp = rand.nextInt(99);
-
-			if(hlp < 40) //minor or treasure art
-			{
-				info.resize(1);
-				loadRandomArtifact(rand, info[0], 10, 10, 0, 0);
-				info[0].limiter.numOfGrants = 1;
-				info[0].message.addTxt(MetaString::ADVOB_TXT, 155);
-				info[0].message.addReplacement(VLC->arth->objects[info[0].reward.artifacts.back()]->getNameTranslated());
-			}
-			else if(hlp < 90) //2 - 5 of non-gold resource
-			{
-				info.resize(1);
-				int type  = rand.nextInt(5);
-				int value = rand.nextInt(2, 5);
-				info[0].reward.resources[type] = value;
-				info[0].limiter.numOfGrants = 1;
-				info[0].message.addTxt(MetaString::ADVOB_TXT, 154);
-			}
-			// or nothing
-		}
-		break;
-	}
-}
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-CGVisitableOPH::CGVisitableOPH()
-{
-	visitMode = VISIT_HERO;
-	selectMode = SELECT_PLAYER;
-}
-
-void CGVisitableOPH::initObj(CRandomGenerator & rand)
-{
-	switch(ID)
-	{
-		case Obj::ARENA:
-			info.resize(2);
-			info[0].reward.primary[PrimarySkill::ATTACK] = 2;
-			info[1].reward.primary[PrimarySkill::DEFENSE] = 2;
-			onSelect.addTxt(MetaString::ADVOB_TXT, 0);
-			onVisited.addTxt(MetaString::ADVOB_TXT, 1);
-			canRefuse = true;
-			break;
-		case Obj::MERCENARY_CAMP:
-			info.resize(1);
-			info[0].reward.primary[PrimarySkill::ATTACK] = 1;
-			info[0].message.addTxt(MetaString::ADVOB_TXT, 80);
-			onVisited.addTxt(MetaString::ADVOB_TXT, 81);
-			break;
-		case Obj::MARLETTO_TOWER:
-			info.resize(1);
-			info[0].reward.primary[PrimarySkill::DEFENSE] = 1;
-			info[0].message.addTxt(MetaString::ADVOB_TXT, 39);
-			onVisited.addTxt(MetaString::ADVOB_TXT, 40);
-			break;
-		case Obj::STAR_AXIS:
-			info.resize(1);
-			info[0].reward.primary[PrimarySkill::SPELL_POWER] = 1;
-			info[0].message.addTxt(MetaString::ADVOB_TXT, 100);
-			onVisited.addTxt(MetaString::ADVOB_TXT, 101);
-			break;
-		case Obj::GARDEN_OF_REVELATION:
-			info.resize(1);
-			info[0].reward.primary[PrimarySkill::KNOWLEDGE] = 1;
-			info[0].message.addTxt(MetaString::ADVOB_TXT, 59);
-			onVisited.addTxt(MetaString::ADVOB_TXT, 60);
-			break;
-		case Obj::LEARNING_STONE:
-			info.resize(1);
-			info[0].reward.gainedExp = 1000;
-			info[0].message.addTxt(MetaString::ADVOB_TXT, 143);
-			onVisited.addTxt(MetaString::ADVOB_TXT, 144);
-			break;
-		case Obj::TREE_OF_KNOWLEDGE:
-			info.resize(1);
-			canRefuse = true;
-			info[0].reward.gainedLevels = 1;
-			onVisited.addTxt(MetaString::ADVOB_TXT, 147);
-			info.resize(1);
-			switch (rand.nextInt(2))
-			{
-			case 0: // free
-				onSelect.addTxt(MetaString::ADVOB_TXT, 148);
-				break;
-			case 1:
-				info[0].limiter.resources[Res::GOLD] = 2000;
-				info[0].reward.resources[Res::GOLD] = -2000;
-				onSelect.addTxt(MetaString::ADVOB_TXT, 149);
-				onEmpty.addTxt(MetaString::ADVOB_TXT, 150);
-				break;
-			case 2:
-				info[0].limiter.resources[Res::GEMS] = 10;
-				info[0].reward.resources[Res::GEMS] = -10;
-				onSelect.addTxt(MetaString::ADVOB_TXT, 151);
-				onEmpty.addTxt(MetaString::ADVOB_TXT, 152);
-				break;
-			}
-			break;
-		case Obj::LIBRARY_OF_ENLIGHTENMENT:
-		{
-			selectMode = SELECT_FIRST;
-			onVisited.addTxt(MetaString::ADVOB_TXT, 67);
-			onEmpty.addTxt(MetaString::ADVOB_TXT, 68);
-
-			CVisitInfo visit;
-			visit.reward.primary[PrimarySkill::ATTACK] = 2;
-			visit.reward.primary[PrimarySkill::DEFENSE] = 2;
-			visit.reward.primary[PrimarySkill::KNOWLEDGE] = 2;
-			visit.reward.primary[PrimarySkill::SPELL_POWER] = 2;
-			visit.message.addTxt(MetaString::ADVOB_TXT, 66);
-
-			static_assert(SecSkillLevel::LEVELS_SIZE == 4, "Behavior of Library of Enlignment may not be correct");
-			for (int i=0; i<SecSkillLevel::LEVELS_SIZE; i++)
-			{
-				visit.limiter.minLevel = 10 - i * 2;
-				visit.limiter.secondary[SecondarySkill::DIPLOMACY] = i;
-				info.push_back(visit);
-			}
-			break;
-		}
-		case Obj::SCHOOL_OF_MAGIC:
-			info.resize(2);
-			info[0].reward.primary[PrimarySkill::SPELL_POWER] = 1;
-			info[1].reward.primary[PrimarySkill::KNOWLEDGE] = 1;
-			info[0].limiter.resources[Res::GOLD] = 1000;
-			info[0].reward.resources[Res::GOLD] = -1000;
-			info[1].limiter.resources[Res::GOLD] = 1000;
-			info[1].reward.resources[Res::GOLD] = -1000;
-			onSelect.addTxt(MetaString::ADVOB_TXT, 71);
-			onVisited.addTxt(MetaString::ADVOB_TXT, 72);
-			onEmpty.addTxt(MetaString::ADVOB_TXT, 73);
-			canRefuse = true;
-			break;
-		case Obj::SCHOOL_OF_WAR:
-			info.resize(2);
-			info[0].reward.primary[PrimarySkill::ATTACK] = 1;
-			info[1].reward.primary[PrimarySkill::DEFENSE] = 1;
-			info[0].limiter.resources[Res::GOLD] = 1000;
-			info[0].reward.resources[Res::GOLD] = -1000;
-			info[1].limiter.resources[Res::GOLD] = 1000;
-			info[1].reward.resources[Res::GOLD] = -1000;
-			onSelect.addTxt(MetaString::ADVOB_TXT, 158);
-			onVisited.addTxt(MetaString::ADVOB_TXT, 159);
-			onEmpty.addTxt(MetaString::ADVOB_TXT, 160);
-			canRefuse = true;
-			break;
-	}
-}
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-CGVisitableOPW::CGVisitableOPW()
-{
-	visitMode = VISIT_ONCE;
-	resetDuration = 7;
-}
-
-void CGVisitableOPW::triggerRewardReset() const
-{
-	CRewardableObject::triggerRewardReset();
-
-	ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_CLEAR, id);
-	cb->sendAndApply(&cov);
-}
-
-void CGVisitableOPW::initObj(CRandomGenerator & rand)
-{
-	setRandomReward(rand);
-
-	switch (ID)
-	{
-	case Obj::MYSTICAL_GARDEN:
-		onEmpty.addTxt(MetaString::ADVOB_TXT, 93);
-		info[0].message.addTxt(MetaString::ADVOB_TXT, 92);
-		break;
-	case Obj::WINDMILL:
-		onEmpty.addTxt(MetaString::ADVOB_TXT, 169);
-		info[0].message.addTxt(MetaString::ADVOB_TXT, 170);
-		break;
-	case Obj::WATER_WHEEL:
-		onEmpty.addTxt(MetaString::ADVOB_TXT, 165);
-		info[0].message.addTxt(MetaString::ADVOB_TXT, 164);
-		break;
-	}
-}
-
-void CGVisitableOPW::setPropertyDer(ui8 what, ui32 val)
-{
-	if(what == ObjProperty::REWARD_RESET)
-	{
-		setRandomReward(cb->gameState()->getRandomGenerator());
-
-		if (ID == Obj::WATER_WHEEL)
-		{
-			auto& reward = info[0].reward.resources[Res::GOLD];
-			if(cb->getDate() > 7)
-			{
-				reward = 1000;
-			}
-			else
-			{
-				reward = 500;
-			}
-		}
-	}
-
-	CRewardableObject::setPropertyDer(what, val);
-}
-
-void CGVisitableOPW::setRandomReward(CRandomGenerator &rand)
-{
-	switch (ID)
-	{
-	case Obj::MYSTICAL_GARDEN:
-		info.resize(1);
-		info[0].limiter.numOfGrants = 1;
-		info[0].reward.resources.amin(0);
-		if (rand.nextInt(1) == 0)
-		{
-			info[0].reward.resources[Res::GEMS] = 5;
-		}
-		else
-		{
-			info[0].reward.resources[Res::GOLD] = 500;
-		}
-		break;
-	case Obj::WINDMILL:
-		info.resize(1);
-		info[0].reward.resources.amin(0);
-		// 3-6 of any resource but wood and gold
-		info[0].reward.resources[rand.nextInt(Res::MERCURY, Res::GEMS)] = rand.nextInt(3, 6);
-		info[0].limiter.numOfGrants = 1;
-		break;
-	case Obj::WATER_WHEEL:
-		info.resize(1);
-		info[0].reward.resources.amin(0);
-		info[0].reward.resources[Res::GOLD] = 500;
-		info[0].limiter.numOfGrants = 1;
-		break;
-	}
-}
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-void CGMagicSpring::initObj(CRandomGenerator & rand)
-{
-	CVisitInfo visit; // TODO: "player above max mana" limiter. Use logical expressions for limiters?
-	visit.reward.manaPercentage = 200;
-	visit.message.addTxt(MetaString::ADVOB_TXT, 74);
-	info.push_back(visit); // two rewards, one for each entrance
-	info.push_back(visit);
-	onEmpty.addTxt(MetaString::ADVOB_TXT, 75);
-}
-
-std::vector<int3> CGMagicSpring::getVisitableOffsets() const
-{
-	std::vector <int3> visitableTiles;
-
-	for(int y = 0; y < 6; y++)
-		for (int x = 0; x < 8; x++) //starting from left
-			if (appearance->isVisitableAt(x, y))
-				visitableTiles.push_back (int3(x, y , 0));
-
-	return visitableTiles;
-}
-
-int3 CGMagicSpring::getVisitableOffset() const
-{
-	auto visitableTiles = getVisitableOffsets();
-
-	if (visitableTiles.size() != info.size())
-	{
-		logGlobal->warn("Unexpected number of visitable tiles of Magic Spring at %s", pos.toString());
-		return int3(-1,-1,-1);
-	}
-
-	for (size_t i=0; i<visitableTiles.size(); i++)
-	{
-		if (info[i].numOfGrants == 0)
-			return visitableTiles[i];
-	}
-	return visitableTiles[0]; // return *something*. This is valid visitable tile but already used
-}
-
-std::vector<ui32> CGMagicSpring::getAvailableRewards(const CGHeroInstance * hero) const
-{
-	auto tiles = getVisitableOffsets();
-	for (size_t i=0; i<tiles.size(); i++)
-	{
-		if (pos - tiles[i] == hero->visitablePos() && info[i].numOfGrants == 0)
-		{
-			return std::vector<ui32>(1, (ui32)i);
-		}
-	}
-	// hero is either not on visitable tile (should not happen) or tile is already used
-	return std::vector<ui32>();
-}
-
 VCMI_LIB_NAMESPACE_END

+ 125 - 154
lib/mapObjects/CRewardableObject.h

@@ -19,20 +19,30 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class CRandomRewardObjectInfo;
 
+class CRewardLimiter;
+using TRewardLimitersList = std::vector<std::shared_ptr<CRewardLimiter>>;
+
 /// Limiters of rewards. Rewards will be granted to hero only if he satisfies requirements
 /// Note: for this is only a test - it won't remove anything from hero (e.g. artifacts or creatures)
 /// NOTE: in future should (partially) replace seer hut/quest guard quests checks
 class DLL_LINKAGE CRewardLimiter
 {
 public:
-	/// how many times this reward can be granted, 0 for unlimited
-	si32 numOfGrants;
-
 	/// day of week, unused if 0, 1-7 will test for current day of week
 	si32 dayOfWeek;
+	si32 daysPassed;
+
+	/// total experience that hero needs to have
+	si32 heroExperience;
 
 	/// level that hero needs to have
-	si32 minLevel;
+	si32 heroLevel;
+
+	/// mana points that hero needs to have
+	si32 manaPoints;
+
+	/// percentage of mana points that hero needs to have
+	si32 manaPercentage;
 
 	/// resources player needs to have in order to trigger reward
 	TResources resources;
@@ -45,13 +55,25 @@ public:
 	/// Note: does not checks for multiple copies of the same arts
 	std::vector<ArtifactID> artifacts;
 
+	/// Spells that hero must have in the spellbook
+	std::vector<SpellID> spells;
+
 	/// creatures that hero needs to have
 	std::vector<CStackBasicDescriptor> creatures;
 
+	/// sub-limiters, all must pass for this limiter to pass
+	TRewardLimitersList allOf;
+
+	/// sub-limiters, at least one should pass for this limiter to pass
+	TRewardLimitersList anyOf;
+
+	/// sub-limiters, none should pass for this limiter to pass
+	TRewardLimitersList noneOf;
+
 	CRewardLimiter():
-		numOfGrants(0),
 		dayOfWeek(0),
-		minLevel(0),
+		daysPassed(0),
+		heroLevel(0),
 		primary(4, 0)
 	{}
 
@@ -59,14 +81,47 @@ public:
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
-		h & numOfGrants;
 		h & dayOfWeek;
-		h & minLevel;
+		h & daysPassed;
+		h & heroExperience;
+		h & heroLevel;
+		h & manaPoints;
+		h & manaPercentage;
 		h & resources;
 		h & primary;
 		h & secondary;
 		h & artifacts;
 		h & creatures;
+		h & allOf;
+		h & anyOf;
+		h & noneOf;
+	}
+};
+
+class DLL_LINKAGE CRewardResetInfo
+{
+public:
+	CRewardResetInfo()
+		: period(0)
+		, visitors(false)
+		, rewards(false)
+	{}
+
+	/// if above zero, object state will be reset each resetDuration days
+	ui32 period;
+
+	/// if true - reset list of visitors (heroes & players) on reset
+	bool visitors;
+
+
+	/// if true - re-randomize rewards on a new week
+	bool rewards;
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & period;
+		h & visitors;
+		h & rewards;
 	}
 };
 
@@ -79,12 +134,16 @@ public:
 	TResources resources;
 
 	/// received experience
-	ui32 gainedExp;
+	si32 heroExperience;
 	/// received levels (converted into XP during grant)
-	ui32 gainedLevels;
+	si32 heroLevel;
 
 	/// mana given to/taken from hero, fixed value
 	si32 manaDiff;
+
+	/// if giving mana points puts hero above mana pool, any overflow will be multiplied by specified percentage
+	si32 manaOverflowFactor;
+
 	/// fixed value, in form of percentage from max
 	si32 manaPercentage;
 
@@ -100,6 +159,9 @@ public:
 	std::vector<si32> primary;
 	std::map<SecondarySkill, si32> secondary;
 
+	/// creatures that will be changed in hero's army
+	std::map<CreatureID, CreatureID> creaturesChange;
+
 	/// objects that hero may receive
 	std::vector<ArtifactID> artifacts;
 	std::vector<SpellID> spells;
@@ -117,8 +179,8 @@ public:
 	Component getDisplayedComponent(const CGHeroInstance * h) const;
 
 	CRewardInfo() :
-		gainedExp(0),
-		gainedLevels(0),
+		heroExperience(0),
+		heroLevel(0),
 		manaDiff(0),
 		manaPercentage(-1),
 		movePoints(0),
@@ -134,9 +196,10 @@ public:
 		h & removeObject;
 		h & manaPercentage;
 		h & movePercentage;
-		h & gainedExp;
-		h & gainedLevels;
+		h & heroExperience;
+		h & heroLevel;
 		h & manaDiff;
+		h & manaOverflowFactor;
 		h & movePoints;
 		h & primary;
 		h & secondary;
@@ -144,42 +207,44 @@ public:
 		h & artifacts;
 		h & spells;
 		h & creatures;
+		h & creaturesChange;
 	}
 };
 
-class DLL_LINKAGE CVisitInfo
+class DLL_LINKAGE CRewardVisitInfo
 {
 public:
+	enum ERewardEventType
+	{
+		EVENT_INVALID,
+		EVENT_FIRST_VISIT,
+		EVENT_ALREADY_VISITED,
+		EVENT_NOT_AVAILABLE
+	};
+
 	CRewardLimiter limiter;
 	CRewardInfo reward;
 
 	/// Message that will be displayed on granting of this reward, if not empty
 	MetaString message;
 
-	/// Chance for this reward to be selected in case of random choice
-	si32 selectChance;
+	/// Event to which this reward is assigned
+	ERewardEventType visitType;
 
-	/// How many times this reward has been granted since last reset
-	si32 numOfGrants;
-
-	CVisitInfo():
-		selectChance(0),
-		numOfGrants(0)
-	{}
+	CRewardVisitInfo() = default;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & limiter;
 		h & reward;
 		h & message;
-		h & selectChance;
-		h & numOfGrants;
+		h & visitType;
 	}
 };
 
 namespace Rewardable
 {
-	const std::array<std::string, 3> SelectModeString{"selectFirst", "selectPlayer", "selectRandom"};
+	const std::array<std::string, 3> SelectModeString{"selectFirst", "selectPlayer"};
 	const std::array<std::string, 5> VisitModeString{"unlimited", "once", "hero", "bonus", "player"};
 }
 
@@ -188,20 +253,12 @@ namespace Rewardable
 class DLL_LINKAGE CRewardableObject : public CArmedInstance
 {
 	/// function that must be called if hero got level-up during grantReward call
-	void grantRewardAfterLevelup(const CVisitInfo & reward, const CGHeroInstance * hero) const;
+	void grantRewardAfterLevelup(const CRewardVisitInfo & reward, const CGHeroInstance * hero) const;
 
 	/// grants reward to hero
-	void grantRewardBeforeLevelup(const CVisitInfo & reward, const CGHeroInstance * hero) const;
-
-protected:
-	/// controls selection of reward granted to player
-	enum ESelectMode
-	{
-		SELECT_FIRST,  // first reward that matches limiters
-		SELECT_PLAYER, // player can select from all allowed rewards
-		SELECT_RANDOM  // reward will be selected from allowed randomly
-	};
+	void grantRewardBeforeLevelup(const CRewardVisitInfo & reward, const CGHeroInstance * hero) const;
 
+public:
 	enum EVisitMode
 	{
 		VISIT_UNLIMITED, // any number of times. Side effect - object hover text won't contain visited/not visited text
@@ -211,37 +268,52 @@ protected:
 		VISIT_PLAYER     // every player can visit object once
 	};
 
-	/// filters list of visit info and returns rewards that can be granted to current hero
-	virtual std::vector<ui32> getAvailableRewards(const CGHeroInstance * hero) const;
-
-	virtual void grantReward(ui32 rewardID, const CGHeroInstance * hero) const;
+protected:
+	/// controls selection of reward granted to player
+	enum ESelectMode
+	{
+		SELECT_FIRST,  // first reward that matches limiters
+		SELECT_PLAYER, // player can select from all allowed rewards
+	};
 
-	virtual CVisitInfo getVisitInfo(int index, const CGHeroInstance *h) const;
+	/// filters list of visit info and returns rewards that can be granted to current hero
+	virtual std::vector<ui32> getAvailableRewards(const CGHeroInstance * hero, CRewardVisitInfo::ERewardEventType event ) const;
 
-	virtual void triggerRewardReset() const;
+	virtual void grantReward(ui32 rewardID, const CGHeroInstance * hero, bool markVisited) const;
 
-	/// Rewards that can be granted by an object
-	std::vector<CVisitInfo> info;
+	virtual void triggerReset() const;
 
-	/// MetaString's that contain text for messages for specific situations
+	/// Message that will be shown if player needs to select one of multiple rewards
 	MetaString onSelect;
-	MetaString onVisited;
-	MetaString onEmpty;
+
+	/// Rewards that can be applied by an object
+	std::vector<CRewardVisitInfo> info;
 
 	/// how reward will be selected, uses ESelectMode enum
 	ui8 selectMode;
+
 	/// contols who can visit an object, uses EVisitMode enum
 	ui8 visitMode;
+
 	/// reward selected by player
 	ui16 selectedReward;
 
-	/// object visitability info will be reset each resetDuration days
-	ui16 resetDuration;
+	/// how and when should the object be reset
+	CRewardResetInfo resetParameters;
 
 	/// if true - player can refuse visiting an object (e.g. Tomb)
 	bool canRefuse;
 
+	/// return true if this object was "cleared" before and no longer has rewards applicable to selected hero
+	/// unlike wasVisited, this method uses information not available to player owner, for example, if object was cleared by another player before
+	bool wasVisitedBefore(const CGHeroInstance * contextHero) const;
+
+	bool onceVisitableObjectCleared;
+
 public:
+	EVisitMode getVisitMode() const;
+	ui16 getResetDuration() const;
+
 	void setPropertyDer(ui8 what, ui32 val) override;
 	std::string getHoverText(PlayerColor player) const override;
 	std::string getHoverText(const CGHeroInstance * hero) const override;
@@ -262,9 +334,6 @@ public:
 	/// applies player selection of reward
 	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
 
-	/// function that will be called once reward is fully granted to hero
-	virtual void onRewardGiven(const CGHeroInstance * hero) const;
-	
 	void initObj(CRandomGenerator & rand) override;
 
 	CRewardableObject();
@@ -274,116 +343,18 @@ public:
 		h & static_cast<CArmedInstance&>(*this);
 		h & info;
 		h & canRefuse;
-		h & resetDuration;
+		h & resetParameters;
 		h & onSelect;
-		h & onVisited;
-		h & onEmpty;
 		h & visitMode;
 		h & selectMode;
 		h & selectedReward;
+		h & onceVisitableObjectCleared;
 	}
 
 	// for configuration/object setup
 	friend class CRandomRewardObjectInfo;
 };
 
-class DLL_LINKAGE CGPickable : public CRewardableObject //campfire, treasure chest, Flotsam, Shipwreck Survivor, Sea Chest
-{
-public:
-	void initObj(CRandomGenerator & rand) override;
-
-	CGPickable();
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CRewardableObject&>(*this);
-	}
-};
-
-class DLL_LINKAGE CGBonusingObject : public CRewardableObject //objects giving bonuses to luck/morale/movement
-{
-protected:
-	CVisitInfo getVisitInfo(int index, const CGHeroInstance *h) const override;
-
-	void grantReward(ui32 rewardID, const CGHeroInstance * hero) const override;
-
-public:
-	void initObj(CRandomGenerator & rand) override;
-
-	CGBonusingObject();
-
-	void onHeroVisit(const CGHeroInstance *h) const override;
-
-	bool wasVisited(const CGHeroInstance * h) const override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CRewardableObject&>(*this);
-	}
-};
-
-class DLL_LINKAGE CGOnceVisitable : public CRewardableObject // wagon, corpse, lean to, warriors tomb
-{
-public:
-	void initObj(CRandomGenerator & rand) override;
-
-	CGOnceVisitable();
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CRewardableObject&>(*this);
-	}
-};
-
-class DLL_LINKAGE CGVisitableOPH : public CRewardableObject //objects visitable only once per hero
-{
-public:
-	void initObj(CRandomGenerator & rand) override;
-
-	CGVisitableOPH();
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CRewardableObject&>(*this);
-	}
-};
-
-class DLL_LINKAGE CGVisitableOPW : public CRewardableObject //objects visitable once per week
-{
-protected:
-	void triggerRewardReset() const override;
-
-public:
-	void initObj(CRandomGenerator & rand) override;
-
-	CGVisitableOPW();
-
-	void setPropertyDer(ui8 what, ui32 val) override;
-	void setRandomReward(CRandomGenerator & rand);
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CRewardableObject&>(*this);
-	}
-};
-
-///Special case - magic spring that has two separate visitable entrances
-class DLL_LINKAGE CGMagicSpring : public CGVisitableOPW
-{
-protected:
-	std::vector<ui32> getAvailableRewards(const CGHeroInstance * hero) const override;
-
-public:
-	void initObj(CRandomGenerator & rand) override;
-	std::vector<int3> getVisitableOffsets() const;
-	int3 getVisitableOffset() const override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CGVisitableOPW&>(*this);
-	}
-};
-
 //TODO:
 
 // MAX

+ 43 - 3
lib/mapObjects/JsonRandom.cpp

@@ -32,15 +32,45 @@ namespace JsonRandom
 		if (value.isNumber())
 			return static_cast<si32>(value.Float());
 		if (!value["amount"].isNull())
-			return static_cast<si32>(value["amount"].Float());
+			return static_cast<si32>(loadValue(value["amount"], rng, defaultValue));
 		si32 min = static_cast<si32>(value["min"].Float());
 		si32 max = static_cast<si32>(value["max"].Float());
 		return rng.getIntRange(min, max)();
 	}
 
+	DLL_LINKAGE std::string loadKey(const JsonNode & value, CRandomGenerator & rng, std::string defaultValue)
+	{
+		if (value.isNull())
+			return defaultValue;
+		if (value.isString())
+			return value.String();
+		if (!value["type"].isNull())
+			return value["type"].String();
+
+		if (value["list"].isNull())
+			return defaultValue;
+
+		const auto & resourceList = value["list"].Vector();
+
+		if (resourceList.empty())
+			return defaultValue;
+
+		si32 index = rng.getIntRange(0, resourceList.size() - 1 )();
+
+		return resourceList[index].String();
+	}
+
 	TResources loadResources(const JsonNode & value, CRandomGenerator & rng)
 	{
 		TResources ret;
+
+		if (value.isVector())
+		{
+			for (const auto & entry : value.Vector())
+				ret += loadResource(entry, rng);
+			return ret;
+		}
+
 		for (size_t i=0; i<GameConstants::RESOURCE_QUANTITY; i++)
 		{
 			ret[i] = loadValue(value[GameConstants::RESOURCE_NAMES[i]], rng);
@@ -48,6 +78,18 @@ namespace JsonRandom
 		return ret;
 	}
 
+	TResources loadResource(const JsonNode & value, CRandomGenerator & rng)
+	{
+		std::string resourceName = loadKey(value, rng, "");
+		si32 resourceAmount = loadValue(value, rng, 0);
+		si32 resourceID(VLC->modh->identifiers.getIdentifier(value.meta, "resource", resourceName).get());
+
+		TResources ret;
+		ret[resourceID] = resourceAmount;
+		return ret;
+	}
+
+
 	std::vector<si32> loadPrimary(const JsonNode & value, CRandomGenerator & rng)
 	{
 		std::vector<si32> ret;
@@ -131,8 +173,6 @@ namespace JsonRandom
 	{
 		if (value.getType() == JsonNode::JsonType::DATA_STRING)
 			return SpellID(VLC->modh->identifiers.getIdentifier("spell", value).get());
-		if (value["type"].getType() == JsonNode::JsonType::DATA_STRING)
-			return SpellID(VLC->modh->identifiers.getIdentifier("spell", value["type"]).get());
 
 		vstd::erase_if(spells, [=](SpellID spell)
 		{

+ 2 - 0
lib/mapObjects/JsonRandom.h

@@ -32,7 +32,9 @@ namespace JsonRandom
 	};
 
 	DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue = 0);
+	DLL_LINKAGE std::string loadKey(const JsonNode & value, CRandomGenerator & rng, std::string defaultValue = "");
 	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);
 

+ 0 - 26
lib/mapObjects/MiscObjects.cpp

@@ -1523,32 +1523,6 @@ void CGWitchHut::serializeJsonOptions(JsonSerializeFormat & handler)
 	}
 }
 
-void CGMagicWell::onHeroVisit( const CGHeroInstance * h ) const
-{
-	int message;
-
-	if(h->hasBonusFrom(Bonus::OBJECT,ID)) //has already visited Well today
-	{
-		message = 78;//"A second drink at the well in one day will not help you."
-	}
-	else if(h->mana < h->manaLimit())
-	{
-		giveDummyBonus(h->id);
-		cb->setManaPoints(h->id,h->manaLimit());
-		message = 77;
-	}
-	else
-	{
-		message = 79;
-	}
-	showInfoDialog(h, message);
-}
-
-std::string CGMagicWell::getHoverText(const CGHeroInstance * hero) const
-{
-	return getObjectName() + " " + visitedTxt(hero->hasBonusFrom(Bonus::OBJECT,ID));
-}
-
 void CGObservatory::onHeroVisit( const CGHeroInstance * h ) const
 {
 	InfoWindow iw;

+ 0 - 12
lib/mapObjects/MiscObjects.h

@@ -391,18 +391,6 @@ public:
 	}
 };
 
-class DLL_LINKAGE CGMagicWell : public CGObjectInstance //objects giving bonuses to luck/morale/movement
-{
-public:
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	std::string getHoverText(const CGHeroInstance * hero) const override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CGObjectInstance&>(*this);
-	}
-};
-
 class DLL_LINKAGE CGSirens : public CGObjectInstance
 {
 public:

+ 0 - 14
lib/registerTypes/RegisterTypes.h

@@ -45,7 +45,6 @@ void registerTypesMapObjects1(Serializer &s)
 			s.template registerType<CGMonolith, CGWhirlpool>();
 	s.template registerType<CGObjectInstance, CGSignBottle>();
 	s.template registerType<CGObjectInstance, CGScholar>();
-	s.template registerType<CGObjectInstance, CGMagicWell>();
 	s.template registerType<CGObjectInstance, CGObservatory>();
 	s.template registerType<CGObjectInstance, CGKeys>();
 		s.template registerType<CGKeys, CGKeymasterTent>();
@@ -99,7 +98,6 @@ void registerTypesMapObjectTypes(Serializer &s)
 	REGISTER_GENERIC_HANDLER(CGArtifact);
 	REGISTER_GENERIC_HANDLER(CGBlackMarket);
 	REGISTER_GENERIC_HANDLER(CGBoat);
-	REGISTER_GENERIC_HANDLER(CGBonusingObject);
 	REGISTER_GENERIC_HANDLER(CGBorderGate);
 	REGISTER_GENERIC_HANDLER(CGBorderGuard);
 	REGISTER_GENERIC_HANDLER(CGCreature);
@@ -113,15 +111,11 @@ void registerTypesMapObjectTypes(Serializer &s)
 	REGISTER_GENERIC_HANDLER(CGLighthouse);
 	REGISTER_GENERIC_HANDLER(CGTerrainPatch);
 	REGISTER_GENERIC_HANDLER(CGMagi);
-	REGISTER_GENERIC_HANDLER(CGMagicSpring);
-	REGISTER_GENERIC_HANDLER(CGMagicWell);
 	REGISTER_GENERIC_HANDLER(CGMarket);
 	REGISTER_GENERIC_HANDLER(CGMine);
 	REGISTER_GENERIC_HANDLER(CGObelisk);
 	REGISTER_GENERIC_HANDLER(CGObservatory);
-	REGISTER_GENERIC_HANDLER(CGOnceVisitable);
 	REGISTER_GENERIC_HANDLER(CGPandoraBox);
-	REGISTER_GENERIC_HANDLER(CGPickable);
 	REGISTER_GENERIC_HANDLER(CGQuestGuard);
 	REGISTER_GENERIC_HANDLER(CGResource);
 	REGISTER_GENERIC_HANDLER(CGScholar);
@@ -135,8 +129,6 @@ void registerTypesMapObjectTypes(Serializer &s)
 	REGISTER_GENERIC_HANDLER(CGWhirlpool);
 	REGISTER_GENERIC_HANDLER(CGTownInstance);
 	REGISTER_GENERIC_HANDLER(CGUniversity);
-	REGISTER_GENERIC_HANDLER(CGVisitableOPH);
-	REGISTER_GENERIC_HANDLER(CGVisitableOPW);
 	REGISTER_GENERIC_HANDLER(CGWitchHut);
 
 #undef REGISTER_GENERIC_HANDLER
@@ -162,12 +154,6 @@ void registerTypesMapObjects2(Serializer &s)
 			s.template registerType<CGTownBuilding, COPWBonus>();
 
 	s.template registerType<CGObjectInstance, CRewardableObject>();
-		s.template registerType<CRewardableObject, CGPickable>();
-		s.template registerType<CRewardableObject, CGBonusingObject>();
-		s.template registerType<CRewardableObject, CGVisitableOPH>();
-		s.template registerType<CRewardableObject, CGVisitableOPW>();
-		s.template registerType<CRewardableObject, CGOnceVisitable>();
-			s.template registerType<CGVisitableOPW, CGMagicSpring>();
 
 	s.template registerType<CGObjectInstance, CTeamVisited>();
 		s.template registerType<CTeamVisited, CGWitchHut>();