Forráskód Böngészése

Merge pull request #3582 from IvanSavenko/campaign_fixes

Campaign fixes
Ivan Savenko 1 éve
szülő
commit
f329bfbe71
52 módosított fájl, 549 hozzáadás és 152 törlés
  1. 2 0
      AI/BattleAI/BattleEvaluator.cpp
  2. 1 0
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  3. 1 1
      AI/Nullkiller/Goals/CompleteQuest.cpp
  4. 1 0
      AI/StupidAI/StupidAI.cpp
  5. 1 1
      AI/VCAI/Goals/CompleteQuest.cpp
  6. 1 0
      Mods/vcmi/config/vcmi/english.json
  7. 1 1
      client/adventureMap/CList.cpp
  8. 1 0
      client/battle/BattleActionsController.cpp
  9. 1 0
      client/battle/BattleStacksController.cpp
  10. 1 1
      client/lobby/CSelectionBase.cpp
  11. 1 5
      client/windows/CCastleInterface.cpp
  12. 3 3
      client/windows/CHeroWindow.cpp
  13. 4 4
      client/windows/GUIClasses.cpp
  14. 211 0
      config/mapOverrides.json
  15. 19 0
      include/vstd/RNG.h
  16. 1 0
      lib/CCreatureHandler.cpp
  17. 1 1
      lib/CCreatureHandler.h
  18. 0 14
      lib/CGameInfoCallback.cpp
  19. 0 2
      lib/CGameInfoCallback.h
  20. 1 1
      lib/CGeneralTextHandler.h
  21. 5 4
      lib/CHeroHandler.cpp
  22. 6 0
      lib/CPlayerState.h
  23. 1 1
      lib/IGameCallback.h
  24. 2 0
      lib/battle/BattleInfo.cpp
  25. 1 0
      lib/battle/CBattleInfoCallback.cpp
  26. 22 9
      lib/campaign/CampaignHandler.cpp
  27. 1 1
      lib/campaign/CampaignHandler.h
  28. 5 2
      lib/campaign/CampaignState.cpp
  29. 6 1
      lib/campaign/CampaignState.h
  30. 3 0
      lib/constants/EntityIdentifiers.cpp
  31. 3 0
      lib/constants/EntityIdentifiers.h
  32. 2 2
      lib/filesystem/CCompressedStream.cpp
  33. 6 0
      lib/filesystem/CCompressedStream.h
  34. 1 4
      lib/gameState/CGameState.cpp
  35. 1 0
      lib/gameState/CGameState.h
  36. 107 41
      lib/gameState/CGameStateCampaign.cpp
  37. 10 4
      lib/gameState/CGameStateCampaign.h
  38. 1 0
      lib/mapObjectConstructors/CBankInstanceConstructor.cpp
  39. 1 0
      lib/mapObjects/CGCreature.cpp
  40. 64 18
      lib/mapObjects/CGHeroInstance.cpp
  41. 7 0
      lib/mapObjects/CGHeroInstance.h
  42. 7 6
      lib/mapObjects/CQuest.cpp
  43. 2 2
      lib/mapObjects/CQuest.h
  44. 11 0
      lib/mapping/CMap.cpp
  45. 9 1
      lib/mapping/CMap.h
  46. 2 0
      lib/mapping/MapFormatH3M.cpp
  47. 3 21
      lib/networkPacks/NetPacksLib.cpp
  48. 1 0
      lib/rewardable/Info.cpp
  49. 3 1
      lib/serializer/ESerializationVersion.h
  50. 1 0
      lib/spells/BattleSpellMechanics.cpp
  51. 1 0
      lib/spells/effects/Catapult.cpp
  52. 2 0
      server/NetPacksLobbyServer.cpp

+ 2 - 0
AI/BattleAI/BattleEvaluator.cpp

@@ -22,6 +22,8 @@
 #include "../../lib/battle/BattleStateInfoForRetreat.h"
 #include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/battle/BattleAction.h"
+#include "../../lib/CRandomGenerator.h"
+
 
 // TODO: remove
 // Eventually only IBattleInfoCallback and battle::Unit should be used,

+ 1 - 0
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -11,6 +11,7 @@
 #include "DangerHitMapAnalyzer.h"
 
 #include "../Engine/Nullkiller.h"
+#include "../../../lib/CRandomGenerator.h"
 
 namespace NKAI
 {

+ 1 - 1
AI/Nullkiller/Goals/CompleteQuest.cpp

@@ -210,7 +210,7 @@ TGoalVec CompleteQuest::missionResources() const
 
 TGoalVec CompleteQuest::missionDestroyObj() const
 {
-	auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget);
+	auto obj = cb->getObj(q.quest->killTarget);
 
 	if(!obj)
 		return CaptureObjectsBehavior(q.obj).decompose();

+ 1 - 0
AI/StupidAI/StupidAI.cpp

@@ -15,6 +15,7 @@
 #include "../../lib/CCreatureHandler.h"
 #include "../../lib/battle/BattleAction.h"
 #include "../../lib/battle/BattleInfo.h"
+#include "../../lib/CRandomGenerator.h"
 
 CStupidAI::CStupidAI()
 	: side(-1)

+ 1 - 1
AI/VCAI/Goals/CompleteQuest.cpp

@@ -241,7 +241,7 @@ TGoalVec CompleteQuest::missionDestroyObj() const
 {
 	TGoalVec solutions;
 
-	auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget);
+	auto obj = cb->getObj(q.quest->killTarget);
 
 	if(!obj)
 		return ai->ah->howToVisitObj(q.obj);

+ 1 - 0
Mods/vcmi/config/vcmi/english.json

@@ -335,6 +335,7 @@
 	"vcmi.map.victoryCondition.collectArtifacts.message" : "Acquire Three Artifacts",
 	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Congratulations! All your enemies have been defeated and you have Angelic Alliance! Victory is yours!",
 	"vcmi.map.victoryCondition.angelicAlliance.message" : "Defeat All Enemies and create Angelic Alliance",
+	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Alas, you have lost part of the Angelic Alliance. All is lost.",
 
 	// few strings from WoG used by vcmi
 	"vcmi.stackExperience.description" : "» S t a c k   E x p e r i e n c e   D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i",

+ 1 - 1
client/adventureMap/CList.cpp

@@ -263,7 +263,7 @@ void CHeroList::CHeroItem::showTooltip()
 
 std::string CHeroList::CHeroItem::getHoverText()
 {
-	return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->type->heroClass->getNameTranslated());
+	return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->getClassNameTranslated());
 }
 
 void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition)

+ 1 - 0
client/battle/BattleActionsController.cpp

@@ -29,6 +29,7 @@
 #include "../../CCallback.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CRandomGenerator.h"
 #include "../../lib/CStack.h"
 #include "../../lib/battle/BattleAction.h"
 #include "../../lib/spells/CSpellHandler.h"

+ 1 - 0
client/battle/BattleStacksController.cpp

@@ -37,6 +37,7 @@
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/battle/BattleAction.h"
 #include "../../lib/battle/BattleHex.h"
+#include "../../lib/CRandomGenerator.h"
 #include "../../lib/CStack.h"
 #include "../../lib/CondSh.h"
 #include "../../lib/TextOperations.h"

+ 1 - 1
client/lobby/CSelectionBase.cpp

@@ -22,7 +22,6 @@
 #include "../CPlayerInterface.h"
 #include "../CMusicHandler.h"
 #include "../CVideoHandler.h"
-#include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
@@ -43,6 +42,7 @@
 
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CHeroHandler.h"
+#include "../../lib/CRandomGenerator.h"
 #include "../../lib/CThreadHelper.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/mapping/CMapInfo.h"

+ 1 - 5
client/windows/CCastleInterface.cpp

@@ -967,11 +967,7 @@ void CCastleBuildings::enterMagesGuild()
 
 	if(hero && !hero->hasSpellbook()) //hero doesn't have spellbok
 	{
-		const StartInfo *si = LOCPLINT->cb->getStartInfo();
-		// it would be nice to find a way to move this hack to config/mapOverrides.json
-		if(si && si->campState &&                                   // We're in campaign,
-			(si->campState->getFilename() == "DATA/YOG.H3C") && // which is "Birth of a Barbarian",
-			(hero->getHeroType() == 45))                        // and the hero is Yog (based on Solmyr)
+		if(hero->isCampaignYog())
 		{
 			// "Yog has given up magic in all its forms..."
 			LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[736]);

+ 3 - 3
client/windows/CHeroWindow.cpp

@@ -190,7 +190,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
 	assert(hero == curHero);
 
 	name->setText(curHero->getNameTranslated());
-	title->setText((boost::format(CGI->generaltexth->allTexts[342]) % curHero->level % curHero->type->heroClass->getNameTranslated()).str());
+	title->setText((boost::format(CGI->generaltexth->allTexts[342]) % curHero->level % curHero->getClassNameTranslated()).str());
 
 	specArea->text = curHero->type->getSpecialtyDescriptionTranslated();
 	specImage->setFrame(curHero->type->imageIndex);
@@ -199,8 +199,8 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
 	tacticsButton = std::make_shared<CToggleButton>(Point(539, 483), AnimationPath::builtin("hsbtns8.def"), std::make_pair(heroscrn[26], heroscrn[31]), 0, EShortcut::HERO_TOGGLE_TACTICS);
 	tacticsButton->addHoverText(CButton::HIGHLIGHTED, CGI->generaltexth->heroscrn[25]);
 
-	dismissButton->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated()));
-	portraitArea->hoverText = boost::str(boost::format(CGI->generaltexth->allTexts[15]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated());
+	dismissButton->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->getClassNameTranslated()));
+	portraitArea->hoverText = boost::str(boost::format(CGI->generaltexth->allTexts[15]) % curHero->getNameTranslated() % curHero->getClassNameTranslated());
 	portraitArea->text = curHero->getBiographyTranslated();
 	portraitImage->setFrame(curHero->getIconIndex());
 

+ 4 - 4
client/windows/GUIClasses.cpp

@@ -424,7 +424,7 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std
 	std::string levelTitleText = CGI->generaltexth->translate("core.genrltxt.445");
 	boost::replace_first(levelTitleText, "%s", hero->getNameTranslated());
 	boost::replace_first(levelTitleText, "%d", std::to_string(hero->level));
-	boost::replace_first(levelTitleText, "%s", hero->type->heroClass->getNameTranslated());
+	boost::replace_first(levelTitleText, "%s", hero->getClassNameTranslated());
 
 	levelTitle = std::make_shared<CLabel>(192, 162, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, levelTitleText);
 
@@ -585,7 +585,7 @@ void CTavernWindow::show(Canvas & to)
 
 			//Recruit %s the %s
 			if (!recruit->isBlocked())
-				recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[3]) % sel->h->getNameTranslated() % sel->h->type->heroClass->getNameTranslated()));
+				recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[3]) % sel->h->getNameTranslated() % sel->h->getClassNameTranslated()));
 
 		}
 
@@ -639,7 +639,7 @@ CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const
 		description = CGI->generaltexth->allTexts[215];
 		boost::algorithm::replace_first(description, "%s", h->getNameTranslated());
 		boost::algorithm::replace_first(description, "%d", std::to_string(h->level));
-		boost::algorithm::replace_first(description, "%s", h->type->heroClass->getNameTranslated());
+		boost::algorithm::replace_first(description, "%s", h->getClassNameTranslated());
 		boost::algorithm::replace_first(description, "%d", std::to_string(artifs));
 
 		portrait = std::make_shared<CAnimImage>(AnimationPath::builtin("portraitsLarge"), h->getIconIndex());
@@ -706,7 +706,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 	auto genTitle = [](const CGHeroInstance * h)
 	{
 		boost::format fmt(CGI->generaltexth->allTexts[138]);
-		fmt % h->getNameTranslated() % h->level % h->type->heroClass->getNameTranslated();
+		fmt % h->getNameTranslated() % h->level % h->getClassNameTranslated();
 		return boost::str(fmt);
 	};
 

+ 211 - 0
config/mapOverrides.json

@@ -1515,6 +1515,217 @@
 		"victoryIconIndex" : 6,
 		"victoryString" : "core.vcdesc.7"
 	},
+	"data/yog:1" : { // The Meeting
+		"defeatIconIndex" : 1,
+		"defeatString" : "core.lcdesc.3",
+		"triggeredEvents" : {
+			"heroMustSurvive" : {
+				"condition" : [
+					"allOf",
+					[ "isHuman", { "value" : 1 } ],
+					[ "noneOf", [ "control", { "position" : [ 31, 32, 0 ], "type" : "hero" } ] ] // yog
+				],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.5",
+					"type" : "defeat"
+				},
+				"message" : "core.genrltxt.253"
+			},
+			"specialVictory" : {
+				"condition" : [
+					"allOf",
+					[ "isHuman", { "value" : 1 } ],
+					[ "control", { "position" : [ 14, 16, 0 ], "type" : "town" } ]
+				],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.250",
+					"type" : "victory"
+				},
+				"message" : "core.genrltxt.249"
+			},
+			"standardDefeat" : {
+				"condition" : [ "daysWithoutTown", { "value" : 7 } ],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.8",
+					"type" : "defeat"
+				},
+				"message" : "core.genrltxt.7"
+			}
+		},
+		"victoryIconIndex" : 6,
+		"victoryString" : "core.vcdesc.7"
+	},
+	"data/yog:2" : { // A Tough Start
+		"defeatIconIndex" : 1,
+		"defeatString" : "core.lcdesc.3",
+		"triggeredEvents" : {
+			"heroMustSurvive" : {
+				"condition" : [
+					"allOf",
+					[ "isHuman", { "value" : 1 } ],
+					[ "noneOf", [ "control", { "position" : [ 33, 36, 0 ], "type" : "hero" } ] ] // yog
+				],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.5",
+					"type" : "defeat"
+				},
+				"message" : "core.genrltxt.253"
+			},
+			"specialVictory" : {
+				"condition" : [
+					"allOf",
+					[ "isHuman", { "value" : 1 } ],
+					[ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ]
+				],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.281",
+					"type" : "victory"
+				},
+				"message" : "core.genrltxt.280"
+			},
+			"specialDefeat" : {
+				"condition" : [
+					"allOf", 
+					[ "isHuman", { "value" : 1 } ], 
+					[ "anyOf",
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.armorOfWonder" } ] ],
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.sandalsOfTheSaint" } ] ],
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.celestialNecklaceOfBliss" } ] ],
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.lionsShieldOfCourage" } ] ],
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.swordOfJudgement" } ] ],
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.helmOfHeavenlyEnlightenment" } ] ]
+					],
+				],
+				"effect" : { 
+					"messageToSend" : "core.genrltxt.5", 
+					"type" : "defeat"
+				},
+				"message" : "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf"
+			},
+			"standardDefeat" : {
+				"condition" : [ "daysWithoutTown", { "value" : 7 } ],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.8",
+					"type" : "defeat"
+				},
+				"message" : "core.genrltxt.7"
+			}
+		},
+		"victoryIconIndex" : 0,
+		"victoryString" : "core.vcdesc.1"
+	},
+	"data/yog:3" : { // Falor and Terwen
+		"defeatIconIndex" : 1,
+		"defeatString" : "core.lcdesc.3",
+		"triggeredEvents" : {
+			"heroMustSurvive" : {
+				"condition" : [
+					"allOf",
+					[ "isHuman", { "value" : 1 } ],
+					[ "noneOf", [ "control", { "position" : [ 3, 5, 0 ], "type" : "hero" } ] ] // yog
+				],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.5",
+					"type" : "defeat"
+				},
+				"message" : "core.genrltxt.253"
+			},
+			"specialVictory" : {
+				"condition" : [
+					"allOf",
+					[ "isHuman", { "value" : 1 } ],
+					[ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ]
+				],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.281",
+					"type" : "victory"
+				},
+				"message" : "core.genrltxt.280"
+			},
+			"specialDefeat" : {
+				"condition" : [
+					"allOf", 
+					[ "isHuman", { "value" : 1 } ], 
+					[ "anyOf",
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.celestialNecklaceOfBliss" } ] ],
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.lionsShieldOfCourage" } ] ],
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.swordOfJudgement" } ] ],
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.helmOfHeavenlyEnlightenment" } ] ]
+					],
+				],
+				"effect" : { 
+					"messageToSend" : "core.genrltxt.5", 
+					"type" : "defeat"
+				},
+				"message" : "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf"
+			},
+			"standardDefeat" : {
+				"condition" : [ "daysWithoutTown", { "value" : 7 } ],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.8",
+					"type" : "defeat"
+				},
+				"message" : "core.genrltxt.7"
+			}
+		},
+		"victoryIconIndex" : 0,
+		"victoryString" : "core.vcdesc.1"
+	},
+	"data/yog:4" : { // Returning to Bracada
+		"defeatIconIndex" : 1,
+		"defeatString" : "core.lcdesc.3",
+		"triggeredEvents" : {
+			"heroMustSurvive" : {
+				"condition" : [
+					"allOf",
+					[ "isHuman", { "value" : 1 } ],
+					[ "noneOf", [ "control", { "position" : [ 32, 5, 0 ], "type" : "hero" } ] ] // yog
+				],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.5",
+					"type" : "defeat"
+				},
+				"message" : "core.genrltxt.253"
+			},
+			"specialVictory" : {
+				"condition" : [
+					"allOf",
+					[ "isHuman", { "value" : 1 } ],
+					[ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ]
+				],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.281",
+					"type" : "victory"
+				},
+				"message" : "core.genrltxt.280"
+			},
+			"specialDefeat" : {
+				"condition" : [
+					"allOf", 
+					[ "isHuman", { "value" : 1 } ], 
+					[ "anyOf",
+						[ "noneOf", [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.swordOfJudgement" } ] ],
+						//[ "noneOf", [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.helmOfHeavenlyEnlightenment" } ] ]
+					],
+				],
+				"effect" : { 
+					"messageToSend" : "core.genrltxt.5", 
+					"type" : "defeat"
+				},
+				"message" : "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf"
+			},
+			"standardDefeat" : {
+				"condition" : [ "daysWithoutTown", { "value" : 7 } ],
+				"effect" : {
+					"messageToSend" : "core.genrltxt.8",
+					"type" : "defeat"
+				},
+				"message" : "core.genrltxt.7"
+			}
+		},
+		"victoryIconIndex" : 0,
+		"victoryString" : "core.vcdesc.1"
+	},
 	"data/final:3" : { // Final Peace
 		"defeatIconIndex" : 1,
 		"defeatString" : "core.lcdesc.2",

+ 19 - 0
include/vstd/RNG.h

@@ -47,6 +47,25 @@ namespace RandomGeneratorUtil
 		return std::next(container.begin(), rand.getInt64Range(0, container.size() - 1)());
 	}
 
+	template<typename Container>
+	size_t nextItemWeighted(Container & container, vstd::RNG & rand)
+	{
+		assert(!container.empty());
+
+		int64_t totalWeight = std::accumulate(container.begin(), container.end(), 0);
+		assert(totalWeight > 0);
+
+		int64_t roll = rand.getInt64Range(0, totalWeight - 1)();
+
+		for (size_t i = 0; i < container.size(); ++i)
+		{
+			roll -= container[i];
+			if(roll < 0)
+				return i;
+		}
+		return container.size() - 1;
+	}
+
 	template<typename T>
 	void randomShuffle(std::vector<T> & container, vstd::RNG & rand)
 	{

+ 1 - 0
lib/CCreatureHandler.cpp

@@ -14,6 +14,7 @@
 #include "ResourceSet.h"
 #include "filesystem/Filesystem.h"
 #include "VCMI_Lib.h"
+#include "CRandomGenerator.h"
 #include "CTownHandler.h"
 #include "GameSettings.h"
 #include "constants/StringConstants.h"

+ 1 - 1
lib/CCreatureHandler.h

@@ -16,7 +16,6 @@
 #include "GameConstants.h"
 #include "JsonNode.h"
 #include "IHandlerBase.h"
-#include "CRandomGenerator.h"
 #include "Color.h"
 #include "filesystem/ResourcePath.h"
 
@@ -29,6 +28,7 @@ class CLegacyConfigParser;
 class CCreatureHandler;
 class CCreature;
 class JsonSerializeFormat;
+class CRandomGenerator;
 
 class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode
 {

+ 0 - 14
lib/CGameInfoCallback.cpp

@@ -124,20 +124,6 @@ TurnTimerInfo CGameInfoCallback::getPlayerTurnTime(PlayerColor color) const
 	return TurnTimerInfo{};
 }
 
-const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(ObjectInstanceID identifier) const
-{
-	if(gs->map->questIdentifierToId.empty())
-	{
-		//assume that it is VCMI map and quest identifier equals instance identifier
-		return getObj(identifier, true);
-	}
-	else
-	{
-		ERROR_RET_VAL_IF(!vstd::contains(gs->map->questIdentifierToId, identifier.getNum()), "There is no object with such quest identifier!", nullptr);
-		return getObj(gs->map->questIdentifierToId[identifier.getNum()]);
-	}
-}
-
 /************************************************************************/
 /*                                                                      */
 /************************************************************************/

+ 0 - 2
lib/CGameInfoCallback.h

@@ -93,7 +93,6 @@ public:
 //	std::vector <const CGObjectInstance * > getFlaggableObjects(int3 pos) const;
 //	const CGObjectInstance * getTopObj (int3 pos) const;
 //	PlayerColor getOwner(ObjectInstanceID heroID) const;
-//	const CGObjectInstance *getObjByQuestIdentifier(ObjectInstanceID identifier) const; //nullptr if object has been removed (eg. killed)
 
 	//map
 //	int3 guardingCreaturePosition (int3 pos) const;
@@ -190,7 +189,6 @@ public:
 	virtual std::vector <const CGObjectInstance * > getFlaggableObjects(int3 pos) const;
 	virtual const CGObjectInstance * getTopObj (int3 pos) const;
 	virtual PlayerColor getOwner(ObjectInstanceID heroID) const;
-	virtual const CGObjectInstance *getObjByQuestIdentifier(ObjectInstanceID identifier) const; //nullptr if object has been removed (eg. killed)
 
 	//map
 	virtual int3 guardingCreaturePosition (int3 pos) const;

+ 1 - 1
lib/CGeneralTextHandler.h

@@ -227,7 +227,7 @@ public:
 	TextContainerRegistrable(const TextContainerRegistrable & other);
 	TextContainerRegistrable(TextContainerRegistrable && other) noexcept;
 
-	TextContainerRegistrable& operator=(TextContainerRegistrable b) = delete;
+	TextContainerRegistrable& operator=(const TextContainerRegistrable & b) = default;
 };
 
 /// Handles all text-related data in game

+ 5 - 4
lib/CHeroHandler.cpp

@@ -18,6 +18,7 @@
 #include "battle/BattleHex.h"
 #include "CCreatureHandler.h"
 #include "GameSettings.h"
+#include "CRandomGenerator.h"
 #include "CTownHandler.h"
 #include "CSkillHandler.h"
 #include "BattleFieldHandler.h"
@@ -281,12 +282,12 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js
 	fillPrimarySkillData(node, heroClass, PrimarySkill::KNOWLEDGE);
 
 	auto percentSumm = std::accumulate(heroClass->primarySkillLowLevel.begin(), heroClass->primarySkillLowLevel.end(), 0);
-	if(percentSumm != 100)
-		logMod->error("Hero class %s has wrong lowLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm);
+	if(percentSumm <= 0)
+		logMod->error("Hero class %s has wrong lowLevelChance values: must be above zero!", heroClass->identifier, percentSumm);
 
 	percentSumm = std::accumulate(heroClass->primarySkillHighLevel.begin(), heroClass->primarySkillHighLevel.end(), 0);
-	if(percentSumm != 100)
-		logMod->error("Hero class %s has wrong highLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm);
+	if(percentSumm <= 0)
+		logMod->error("Hero class %s has wrong highLevelChance values: must be above zero!", heroClass->identifier, percentSumm);
 
 	for(auto skillPair : node["secondarySkills"].Struct())
 	{

+ 6 - 0
lib/CPlayerState.h

@@ -52,6 +52,10 @@ public:
 	bool human; //true if human controlled player, false for AI
 	TeamID team;
 	TResources resources;
+
+	/// list of objects that were "destroyed" by player, either via simple pick-up (e.g. resources) or defeated heroes or wandering monsters
+	std::set<ObjectInstanceID> destroyedObjects;
+
 	std::set<ObjectInstanceID> visitedObjects; // as a std::set, since most accesses here will be from visited status checks
 	std::set<VisitedObjectGlobal> visitedObjectsGlobal;
 	std::vector<ConstTransitivePtr<CGHeroInstance> > heroes;
@@ -110,6 +114,8 @@ public:
 		h & enteredLosingCheatCode;
 		h & enteredWinningCheatCode;
 		h & static_cast<CBonusSystemNode&>(*this);
+		if (h.version >= Handler::Version::DESTROYED_OBJECTS)
+			h & destroyedObjects;
 	}
 };
 

+ 1 - 1
lib/IGameCallback.h

@@ -12,7 +12,6 @@
 #include <vcmi/Metatype.h>
 
 #include "CGameInfoCallback.h" // for CGameInfoCallback
-#include "CRandomGenerator.h"
 #include "networkPacks/ObjProperty.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -23,6 +22,7 @@ struct BlockingDialog;
 struct TeleportDialog;
 struct StackLocation;
 struct ArtifactLocation;
+class CRandomGenerator;
 class CCreatureSet;
 class CStackBasicDescriptor;
 class CGCreature;

+ 2 - 0
lib/battle/BattleInfo.cpp

@@ -12,6 +12,7 @@
 #include "CObstacleInstance.h"
 #include "bonuses/Limiters.h"
 #include "bonuses/Updaters.h"
+#include "../CRandomGenerator.h"
 #include "../CStack.h"
 #include "../CHeroHandler.h"
 #include "../filesystem/Filesystem.h"
@@ -20,6 +21,7 @@
 #include "../BattleFieldHandler.h"
 #include "../ObstacleHandler.h"
 
+
 //TODO: remove
 #include "../IGameCallback.h"
 

+ 1 - 0
lib/battle/CBattleInfoCallback.cpp

@@ -25,6 +25,7 @@
 #include "../networkPacks/PacksForClientBattle.h"
 #include "../BattleFieldHandler.h"
 #include "../Rect.h"
+#include "../CRandomGenerator.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 22 - 9
lib/campaign/CampaignHandler.cpp

@@ -66,7 +66,7 @@ std::unique_ptr<Campaign> CampaignHandler::getHeader( const std::string & name)
 	
 	auto ret = std::make_unique<Campaign>();
 	auto fileStream = CResourceHandler::get(modName)->load(resourceID);
-	std::vector<ui8> cmpgn = getFile(std::move(fileStream), true)[0];
+	std::vector<ui8> cmpgn = getFile(std::move(fileStream), name, true)[0];
 
 	readCampaign(ret.get(), cmpgn, resourceID.getName(), modName, encoding);
 
@@ -84,7 +84,7 @@ std::shared_ptr<CampaignState> CampaignHandler::getCampaign( const std::string &
 	
 	auto fileStream = CResourceHandler::get(modName)->load(resourceID);
 
-	std::vector<std::vector<ui8>> files = getFile(std::move(fileStream), false);
+	std::vector<std::vector<ui8>> files = getFile(std::move(fileStream), name, false);
 
 	readCampaign(ret.get(), files[0], resourceID.getName(), modName, encoding);
 
@@ -578,19 +578,32 @@ CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & rea
 	return ret;
 }
 
-std::vector< std::vector<ui8> > CampaignHandler::getFile(std::unique_ptr<CInputStream> file, bool headerOnly)
+std::vector< std::vector<ui8> > CampaignHandler::getFile(std::unique_ptr<CInputStream> file, const std::string & filename, bool headerOnly)
 {
 	CCompressedStream stream(std::move(file), true);
 
 	std::vector< std::vector<ui8> > ret;
-	do
+
+	try
+	{
+		do
+		{
+			std::vector<ui8> block(stream.getSize());
+			stream.read(block.data(), block.size());
+			ret.push_back(block);
+			ret.back().shrink_to_fit();
+		}
+		while (!headerOnly && stream.getNextBlock());
+	}
+	catch (const DecompressionException & e)
 	{
-		std::vector<ui8> block(stream.getSize());
-		stream.read(block.data(), block.size());
-		ret.push_back(block);
-		ret.back().shrink_to_fit();
+		// Some campaigns in French version from gog.com have trailing garbage bytes
+		// For example, slayer.h3c consist from 5 parts: header + 4 maps
+		// However file also contains ~100 "extra" bytes after those 5 parts are decompressed that do not represent gzip stream
+		// leading to exception "Incorrect header check"
+		// Since H3 handles these files correctly, simply log this as warning and proceed
+		logGlobal->warn("Failed to read file %s. Encountered error during decompression: %s", filename, e.what());
 	}
-	while (!headerOnly && stream.getNextBlock());
 
 	return ret;
 }

+ 1 - 1
lib/campaign/CampaignHandler.h

@@ -31,7 +31,7 @@ class DLL_LINKAGE CampaignHandler
 	static CampaignTravel readScenarioTravelFromMemory(CBinaryReader & reader, CampaignVersion version);
 	/// returns h3c split in parts. 0 = h3c header, 1-end - maps (binary h3m)
 	/// headerOnly - only header will be decompressed, returned vector wont have any maps
-	static std::vector<std::vector<ui8>> getFile(std::unique_ptr<CInputStream> file, bool headerOnly);
+	static std::vector<std::vector<ui8>> getFile(std::unique_ptr<CInputStream> file, const std::string & filename, bool headerOnly);
 
 	static VideoPath prologVideoName(ui8 index);
 	static AudioPath prologMusicName(ui8 index);

+ 5 - 2
lib/campaign/CampaignState.cpp

@@ -317,7 +317,7 @@ std::optional<ui8> CampaignState::getBonusID(CampaignScenarioID which) const
 	return chosenCampaignBonuses.at(which);
 }
 
-std::unique_ptr<CMap> CampaignState::getMap(CampaignScenarioID scenarioId, IGameCallback * cb) const
+std::unique_ptr<CMap> CampaignState::getMap(CampaignScenarioID scenarioId, IGameCallback * cb)
 {
 	// FIXME: there is certainly better way to handle maps inside campaigns
 	if(scenarioId == CampaignScenarioID::NONE)
@@ -328,7 +328,10 @@ std::unique_ptr<CMap> CampaignState::getMap(CampaignScenarioID scenarioId, IGame
 	boost::to_lower(scenarioName);
 	scenarioName += ':' + std::to_string(scenarioId.getNum());
 	const auto & mapContent = mapPieces.find(scenarioId)->second;
-	return mapService.loadMap(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding(), cb);
+	auto result = mapService.loadMap(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding(), cb);
+
+	mapTranslations[scenarioId] = result->texts;
+	return result;
 }
 
 std::unique_ptr<CMapHeader> CampaignState::getMapHeader(CampaignScenarioID scenarioId) const

+ 6 - 1
lib/campaign/CampaignState.h

@@ -244,6 +244,9 @@ class DLL_LINKAGE CampaignState : public Campaign
 	/// List of all maps completed by player, in order of their completion
 	std::vector<CampaignScenarioID> mapsConquered;
 
+	/// List of previously loaded campaign maps, to prevent translation of transferred hero names getting lost after their original map has been completed
+	std::map<CampaignScenarioID, TextContainerRegistrable> mapTranslations;
+
 	std::map<CampaignScenarioID, std::vector<uint8_t> > mapPieces; //binary h3ms, scenario number -> map data
 	std::map<CampaignScenarioID, ui8> chosenCampaignBonuses;
 	std::optional<CampaignScenarioID> currentMap;
@@ -278,7 +281,7 @@ public:
 	/// Returns true if all available scenarios have been completed and campaign is finished
 	bool isCampaignFinished() const;
 
-	std::unique_ptr<CMap> getMap(CampaignScenarioID scenarioId, IGameCallback * cb) const;
+	std::unique_ptr<CMap> getMap(CampaignScenarioID scenarioId, IGameCallback * cb);
 	std::unique_ptr<CMapHeader> getMapHeader(CampaignScenarioID scenarioId) const;
 	std::shared_ptr<CMapInfo> getMapInfo(CampaignScenarioID scenarioId) const;
 
@@ -314,6 +317,8 @@ public:
 		h & currentMap;
 		h & chosenCampaignBonuses;
 		h & campaignSet;
+		if (h.version >= Handler::Version::CAMPAIGN_MAP_TRANSLATIONS)
+			h & mapTranslations;
 	}
 };
 

+ 3 - 0
lib/constants/EntityIdentifiers.cpp

@@ -53,6 +53,9 @@ const QueryID QueryID::NONE(-1);
 const QueryID QueryID::CLIENT(-2);
 const HeroTypeID HeroTypeID::NONE(-1);
 const HeroTypeID HeroTypeID::RANDOM(-2);
+const HeroTypeID HeroTypeID::GEM(27);
+const HeroTypeID HeroTypeID::SOLMYR(45);
+
 const ObjectInstanceID ObjectInstanceID::NONE(-1);
 
 const SlotID SlotID::COMMANDER_SLOT_PLACEHOLDER(-2);

+ 3 - 0
lib/constants/EntityIdentifiers.h

@@ -102,6 +102,8 @@ public:
 
 	static const HeroTypeID NONE;
 	static const HeroTypeID RANDOM;
+	static const HeroTypeID GEM; // aka Gem, Sorceress in campaign
+	static const HeroTypeID SOLMYR; // aka Young Yog in campaigns
 
 	bool isValid() const
 	{
@@ -646,6 +648,7 @@ public:
 		FIRST_AID_TENT = 6,
 		VIAL_OF_DRAGON_BLOOD = 127,
 		ARMAGEDDONS_BLADE = 128,
+		ANGELIC_ALLIANCE = 129,
 		TITANS_THUNDER = 135,
 		ART_SELECTION = 144,
 		ART_LOCK = 145, // FIXME: We must get rid of this one since it's conflict with artifact from mods. See issue 2455

+ 2 - 2
lib/filesystem/CCompressedStream.cpp

@@ -162,9 +162,9 @@ si64 CCompressedStream::readMore(ui8 *data, si64 size)
 			break;
 		default:
 			if (inflateState->msg == nullptr)
-				throw std::runtime_error("Decompression error. Return code was " + std::to_string(ret));
+				throw DecompressionException("Error code " + std::to_string(ret));
 			else
-				throw std::runtime_error(std::string("Decompression error: ") + inflateState->msg);
+				throw DecompressionException(inflateState->msg);
 		}
 	}
 	while (!endLoop && inflateState->avail_out != 0 );

+ 6 - 0
lib/filesystem/CCompressedStream.h

@@ -15,6 +15,12 @@ struct z_stream_s;
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+class DecompressionException : public std::runtime_error
+{
+public:
+	using runtime_error::runtime_error;
+};
+
 /// Abstract class that provides buffer for one-directional input streams (e.g. compressed data)
 /// Used for zip archives support and in .lod deflate compression
 class CBufferedStream : public CInputStream

+ 1 - 4
lib/gameState/CGameState.cpp

@@ -1420,10 +1420,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio
 		{
 			if (condition.objectID != ObjectInstanceID::NONE) // mode A - destroy specific object of this type
 			{
-				if(const auto * hero = getHero(condition.objectID))
-					return boost::range::find(gs->map->heroesOnMap, hero) == gs->map->heroesOnMap.end();
-				else
-					return getObj(condition.objectID) == nullptr;
+				return p->destroyedObjects.count(condition.objectID);
 			}
 			else
 			{

+ 1 - 0
lib/gameState/CGameState.h

@@ -13,6 +13,7 @@
 #include "IGameCallback.h"
 #include "LoadProgress.h"
 #include "ConstTransitivePtr.h"
+#include "../CRandomGenerator.h"
 
 namespace boost
 {

+ 107 - 41
lib/gameState/CGameStateCampaign.cpp

@@ -60,30 +60,22 @@ std::optional<CampaignScenarioID> CGameStateCampaign::getHeroesSourceScenario()
 	return campaignState->lastScenario();
 }
 
-void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroReplacement> & campaignHeroReplacements, const CampaignTravel & travelOptions)
+void CGameStateCampaign::trimCrossoverHeroesParameters(const CampaignTravel & travelOptions)
 {
-	// create heroes list for convenience iterating
-	std::vector<CGHeroInstance *> crossoverHeroes;
-	crossoverHeroes.reserve(campaignHeroReplacements.size());
-	for(auto & campaignHeroReplacement : campaignHeroReplacements)
-	{
-		crossoverHeroes.push_back(campaignHeroReplacement.hero);
-	}
-
 	// TODO this logic (what should be kept) should be part of CScenarioTravel and be exposed via some clean set of methods
 	if(!travelOptions.whatHeroKeeps.experience)
 	{
 		//trimming experience
-		for(CGHeroInstance * cgh : crossoverHeroes)
+		for(auto & hero : campaignHeroReplacements)
 		{
-			cgh->initExp(gameState->getRandomGenerator());
+			hero.hero->initExp(gameState->getRandomGenerator());
 		}
 	}
 
 	if(!travelOptions.whatHeroKeeps.primarySkills)
 	{
 		//trimming prim skills
-		for(CGHeroInstance * cgh : crossoverHeroes)
+		for(auto & hero : campaignHeroReplacements)
 		{
 			for(auto g = PrimarySkill::BEGIN; g < PrimarySkill::END; ++g)
 			{
@@ -91,7 +83,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroR
 					.And(Selector::subtype()(BonusSubtypeID(g)))
 					.And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL));
 
-				cgh->getLocalBonus(sel)->val = cgh->type->heroClass->primarySkillInitial[g.getNum()];
+				hero.hero->getLocalBonus(sel)->val = hero.hero->type->heroClass->primarySkillInitial[g.getNum()];
 			}
 		}
 	}
@@ -99,32 +91,32 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroR
 	if(!travelOptions.whatHeroKeeps.secondarySkills)
 	{
 		//trimming sec skills
-		for(CGHeroInstance * cgh : crossoverHeroes)
+		for(auto & hero : campaignHeroReplacements)
 		{
-			cgh->secSkills = cgh->type->secSkillsInit;
-			cgh->recreateSecondarySkillsBonuses();
+			hero.hero->secSkills = hero.hero->type->secSkillsInit;
+			hero.hero->recreateSecondarySkillsBonuses();
 		}
 	}
 
 	if(!travelOptions.whatHeroKeeps.spells)
 	{
-		for(CGHeroInstance * cgh : crossoverHeroes)
+		for(auto & hero : campaignHeroReplacements)
 		{
-			cgh->removeSpellbook();
+			hero.hero->removeSpellbook();
 		}
 	}
 
 	if(!travelOptions.whatHeroKeeps.artifacts)
 	{
 		//trimming artifacts
-		for(CGHeroInstance * hero : crossoverHeroes)
+		for(auto & hero : campaignHeroReplacements)
 		{
 			const auto & checkAndRemoveArtifact = [&](const ArtifactPosition & artifactPosition)
 			{
 				if(artifactPosition == ArtifactPosition::SPELLBOOK)
 					return; // do not handle spellbook this way
 
-				const ArtSlotInfo *info = hero->getSlot(artifactPosition);
+				const ArtSlotInfo *info = hero.hero->getSlot(artifactPosition);
 				if(!info)
 					return;
 
@@ -135,24 +127,27 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroR
 
 				bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId());
 
-				ArtifactLocation al(hero->id, artifactPosition);
-				if(!takeable && !hero->getSlot(al.slot)->locked)  //don't try removing locked artifacts -> it crashes #1719
-					hero->getArt(al.slot)->removeFrom(*hero, al.slot);
+				if (takeable)
+					hero.transferrableArtifacts.push_back(artifactPosition);
+
+				ArtifactLocation al(hero.hero->id, artifactPosition);
+				if(!takeable && !hero.hero->getSlot(al.slot)->locked)  //don't try removing locked artifacts -> it crashes #1719
+					hero.hero->getArt(al.slot)->removeFrom(*hero.hero, al.slot);
 			};
 
 			// process on copy - removal of artifact will invalidate container
-			auto artifactsWorn = hero->artifactsWorn;
+			auto artifactsWorn = hero.hero->artifactsWorn;
 			for(const auto & art : artifactsWorn)
 				checkAndRemoveArtifact(art.first);
 
 			// process in reverse - removal of artifact will shift all artifacts after this one
-			for(int slotNumber = hero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--)
+			for(int slotNumber = hero.hero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--)
 				checkAndRemoveArtifact(ArtifactPosition::BACKPACK_START + slotNumber);
 		}
 	}
 
 	//trimming creatures
-	for(CGHeroInstance * cgh : crossoverHeroes)
+	for(auto & hero : campaignHeroReplacements)
 	{
 		auto shouldSlotBeErased = [&](const std::pair<SlotID, CStackInstance *> & j) -> bool
 		{
@@ -160,16 +155,16 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroR
 			return !travelOptions.monstersKeptByHero.count(crid);
 		};
 
-		auto stacksCopy = cgh->stacks; //copy of the map, so we can iterate iover it and remove stacks
+		auto stacksCopy = hero.hero->stacks; //copy of the map, so we can iterate iover it and remove stacks
 		for(auto &slotPair : stacksCopy)
 			if(shouldSlotBeErased(slotPair))
-				cgh->eraseStack(slotPair.first);
+				hero.hero->eraseStack(slotPair.first);
 	}
 
 	// Removing short-term bonuses
-	for(CGHeroInstance * cgh : crossoverHeroes)
+	for(auto & hero : campaignHeroReplacements)
 	{
-		cgh->removeBonusesRecursive(CSelector(Bonus::OneDay)
+		hero.hero->removeBonusesRecursive(CSelector(Bonus::OneDay)
 			.Or(CSelector(Bonus::OneWeek))
 			.Or(CSelector(Bonus::NTurns))
 			.Or(CSelector(Bonus::NDays))
@@ -201,10 +196,10 @@ void CGameStateCampaign::placeCampaignHeroes()
 	}
 
 	logGlobal->debug("\tGenerate list of hero placeholders");
-	auto campaignHeroReplacements = generateCampaignHeroesToReplace();
+	generateCampaignHeroesToReplace();
 
 	logGlobal->debug("\tPrepare crossover heroes");
-	trimCrossoverHeroesParameters(campaignHeroReplacements, campaignState->scenario(*campaignState->currentScenario()).travelOptions);
+	trimCrossoverHeroesParameters(campaignState->scenario(*campaignState->currentScenario()).travelOptions);
 
 	// remove same heroes on the map which will be added through crossover heroes
 	// INFO: we will remove heroes because later it may be possible that the API doesn't allow having heroes
@@ -221,8 +216,9 @@ void CGameStateCampaign::placeCampaignHeroes()
 			heroesToRemove.insert(heroID);
 	}
 
-	for(auto & campaignHeroReplacement : campaignHeroReplacements)
-		heroesToRemove.insert(campaignHeroReplacement.hero->getHeroType());
+	for(auto & replacement : campaignHeroReplacements)
+		if (replacement.heroPlaceholderId.hasValue())
+			heroesToRemove.insert(replacement.hero->getHeroType());
 
 	for(auto & heroID : heroesToRemove)
 	{
@@ -237,7 +233,7 @@ void CGameStateCampaign::placeCampaignHeroes()
 	}
 
 	logGlobal->debug("\tReplace placeholders with heroes");
-	replaceHeroesPlaceholders(campaignHeroReplacements);
+	replaceHeroesPlaceholders();
 
 	// now add removed heroes again with unused type ID
 	for(auto * hero : removedHeroes)
@@ -337,10 +333,13 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero)
 	}
 }
 
-void CGameStateCampaign::replaceHeroesPlaceholders(const std::vector<CampaignHeroReplacement> & campaignHeroReplacements)
+void CGameStateCampaign::replaceHeroesPlaceholders()
 {
 	for(const auto & campaignHeroReplacement : campaignHeroReplacements)
 	{
+		if (!campaignHeroReplacement.heroPlaceholderId.hasValue())
+			continue;
+
 		auto * heroPlaceholder = dynamic_cast<CGHeroPlaceholder *>(gameState->getObjInstance(campaignHeroReplacement.heroPlaceholderId));
 
 		CGHeroInstance *heroToPlace = campaignHeroReplacement.hero;
@@ -364,14 +363,65 @@ void CGameStateCampaign::replaceHeroesPlaceholders(const std::vector<CampaignHer
 	}
 }
 
-std::vector<CampaignHeroReplacement> CGameStateCampaign::generateCampaignHeroesToReplace()
+void CGameStateCampaign::transferMissingArtifacts(const CampaignTravel & travelOptions)
+{
+	CGHeroInstance * receiver = nullptr;
+
+	for(auto obj : gameState->map->objects)
+	{
+		if (!obj)
+			continue;
+
+		if (obj->ID != Obj::HERO)
+			continue;
+
+		auto * hero = dynamic_cast<CGHeroInstance *>(obj.get());
+
+		if (gameState->getPlayerState(hero->getOwner())->isHuman())
+		{
+			receiver = hero;
+			break;
+		}
+	}
+	assert(receiver);
+
+	for(const auto & campaignHeroReplacement : campaignHeroReplacements)
+	{
+		if (campaignHeroReplacement.heroPlaceholderId.hasValue())
+			continue;
+
+		auto * donorHero = campaignHeroReplacement.hero;
+
+		for (auto const & artLocation : campaignHeroReplacement.transferrableArtifacts)
+		{
+			auto * artifact = donorHero->getArt(artLocation);
+			artifact->removeFrom(*donorHero, artLocation);
+
+			if (receiver)
+			{
+				const auto slot = ArtifactUtils::getArtAnyPosition(receiver, artifact->getTypeId());
+				if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot))
+					artifact->putAt(*receiver, slot);
+				else
+					logGlobal->error("Cannot transfer artifact - no free slots!");
+			}
+			else
+				logGlobal->error("Cannot transfer artifact - no receiver hero!");
+		}
+
+		delete donorHero;
+	}
+}
+
+void CGameStateCampaign::generateCampaignHeroesToReplace()
 {
 	auto campaignState = gameState->scenarioOps->campState;
 
-	std::vector<CampaignHeroReplacement> campaignHeroReplacements;
 	std::vector<CGHeroPlaceholder *> placeholdersByPower;
 	std::vector<CGHeroPlaceholder *> placeholdersByType;
 
+	campaignHeroReplacements.clear();
+
 	// find all placeholders on map
 	for(auto obj : gameState->map->objects)
 	{
@@ -412,7 +462,7 @@ std::vector<CampaignHeroReplacement> CGameStateCampaign::generateCampaignHeroesT
 
 	auto lastScenario = getHeroesSourceScenario();
 
-	if (!placeholdersByPower.empty() && lastScenario)
+	if (lastScenario)
 	{
 		// sort hero placeholders descending power
 		boost::range::sort(placeholdersByPower, [](const CGHeroPlaceholder * a, const CGHeroPlaceholder * b)
@@ -435,8 +485,14 @@ std::vector<CampaignHeroReplacement> CGameStateCampaign::generateCampaignHeroesT
 
 			campaignHeroReplacements.emplace_back(hero, placeholder->id);
 		}
+
+		// Add remaining heroes without placeholders - to transfer their artifacts to placed heroes
+		for (;nodeListIter != nodeList.end(); ++nodeListIter)
+		{
+			CGHeroInstance * hero = campaignState->crossoverDeserialize(*nodeListIter, gameState->map);
+			campaignHeroReplacements.emplace_back(hero, ObjectInstanceID::NONE);
+		}
 	}
-	return campaignHeroReplacements;
 }
 
 void CGameStateCampaign::initHeroes()
@@ -485,6 +541,16 @@ void CGameStateCampaign::initHeroes()
 			}
 		}
 	}
+
+	auto campaignState = gameState->scenarioOps->campState;
+	auto * yog = gameState->getUsedHero(HeroTypeID::SOLMYR);
+	if (yog && boost::starts_with(campaignState->getFilename(), "DATA/YOG") && campaignState->currentScenario()->getNum() == 2)
+	{
+		assert(yog->isCampaignYog());
+		gameState->giveHeroArtifact(yog, ArtifactID::ANGELIC_ALLIANCE);
+	}
+
+	transferMissingArtifacts(campaignState->scenario(*campaignState->currentScenario()).travelOptions);
 }
 
 void CGameStateCampaign::initStartingResources()
@@ -598,7 +664,7 @@ bool CGameStateCampaign::playerHasStartingHero(PlayerColor playerColor) const
 	return false;
 }
 
-std::unique_ptr<CMap> CGameStateCampaign::getCurrentMap() const
+std::unique_ptr<CMap> CGameStateCampaign::getCurrentMap()
 {
 	return gameState->scenarioOps->campState->getMap(CampaignScenarioID::NONE, gameState->callback);
 }

+ 10 - 4
lib/gameState/CGameStateCampaign.h

@@ -25,24 +25,30 @@ struct CampaignHeroReplacement
 	CampaignHeroReplacement(CGHeroInstance * hero, const ObjectInstanceID & heroPlaceholderId);
 	CGHeroInstance * hero;
 	ObjectInstanceID heroPlaceholderId;
+	std::vector<ArtifactPosition> transferrableArtifacts;
 };
 
 class CGameStateCampaign
 {
 	CGameState * gameState;
 
+	/// Contains list of heroes that may be available in this scenario
+	/// temporary helper for game initialization, not serialized
+	std::vector<CampaignHeroReplacement> campaignHeroReplacements;
+
 	/// Returns ID of scenario from which hero placeholders should be selected
 	std::optional<CampaignScenarioID> getHeroesSourceScenario() const;
 
 	/// returns heroes and placeholders in where heroes will be put
-	std::vector<CampaignHeroReplacement> generateCampaignHeroesToReplace();
+	void generateCampaignHeroesToReplace();
 
 	std::optional<CampaignBonus> currentBonus() const;
 
 	/// Trims hero parameters that should not transfer between scenarios according to travelOptions flags
-	void trimCrossoverHeroesParameters(std::vector<CampaignHeroReplacement> & campaignHeroReplacements, const CampaignTravel & travelOptions);
+	void trimCrossoverHeroesParameters(const CampaignTravel & travelOptions);
 
-	void replaceHeroesPlaceholders(const std::vector<CampaignHeroReplacement> & campaignHeroReplacements);
+	void replaceHeroesPlaceholders();
+	void transferMissingArtifacts(const CampaignTravel & travelOptions);
 
 	void giveCampaignBonusToHero(CGHeroInstance * hero);
 
@@ -56,7 +62,7 @@ public:
 	void initTowns();
 
 	bool playerHasStartingHero(PlayerColor player) const;
-	std::unique_ptr<CMap> getCurrentMap() const;
+	std::unique_ptr<CMap> getCurrentMap();
 
 	template <typename Handler> void serialize(Handler &h)
 	{

+ 1 - 0
lib/mapObjectConstructors/CBankInstanceConstructor.cpp

@@ -13,6 +13,7 @@
 #include "../JsonRandom.h"
 #include "../CGeneralTextHandler.h"
 #include "../IGameCallback.h"
+#include "../CRandomGenerator.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 1 - 0
lib/mapObjects/CGCreature.cpp

@@ -20,6 +20,7 @@
 #include "../networkPacks/PacksForClientBattle.h"
 #include "../networkPacks/StackLocation.h"
 #include "../serializer/JsonSerializeFormat.h"
+#include "../CRandomGenerator.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 64 - 18
lib/mapObjects/CGHeroInstance.cpp

@@ -13,6 +13,7 @@
 
 #include <vcmi/ServerCallback.h>
 #include <vcmi/spells/Spell.h>
+#include <vstd/RNG.h>
 
 #include "../CGeneralTextHandler.h"
 #include "../ArtifactUtils.h"
@@ -28,7 +29,9 @@
 #include "../CCreatureHandler.h"
 #include "../CTownHandler.h"
 #include "../mapping/CMap.h"
+#include "../StartInfo.h"
 #include "CGTownInstance.h"
+#include "../campaign/CampaignState.h"
 #include "../pathfinder/TurnInfo.h"
 #include "../serializer/JsonSerializeFormat.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
@@ -555,7 +558,7 @@ std::string CGHeroInstance::getObjectName() const
 	{
 		std::string hoverName = VLC->generaltexth->allTexts[15];
 		boost::algorithm::replace_first(hoverName,"%s",getNameTranslated());
-		boost::algorithm::replace_first(hoverName,"%s", type->heroClass->getNameTranslated());
+		boost::algorithm::replace_first(hoverName,"%s", getClassNameTranslated());
 		return hoverName;
 	}
 	else
@@ -1099,6 +1102,18 @@ std::string CGHeroInstance::getNameTranslated() const
 	return VLC->generaltexth->translate(getNameTextID());
 }
 
+std::string CGHeroInstance::getClassNameTranslated() const
+{
+	return VLC->generaltexth->translate(getClassNameTextID());
+}
+
+std::string CGHeroInstance::getClassNameTextID() const
+{
+	if (isCampaignGem())
+		return "core.genrltxt.735";
+	return type->heroClass->getNameTranslated();
+}
+
 std::string CGHeroInstance::getNameTextID() const
 {
 	if (!nameCustomTextId.empty())
@@ -1353,28 +1368,17 @@ std::vector<SecondarySkill> CGHeroInstance::getLevelUpProposedSecondarySkills(CR
 PrimarySkill CGHeroInstance::nextPrimarySkill(CRandomGenerator & rand) const
 {
 	assert(gainsLevel());
-	int randomValue = rand.nextInt(99);
-	int pom = 0;
-	int primarySkill = 0;
 	const auto isLowLevelHero = level < GameConstants::HERO_HIGH_LEVEL;
 	const auto & skillChances = isLowLevelHero ? type->heroClass->primarySkillLowLevel : type->heroClass->primarySkillHighLevel;
 
-	for(; primarySkill < GameConstants::PRIMARY_SKILLS; ++primarySkill)
-	{
-		pom += skillChances[primarySkill];
-		if(randomValue < pom)
-		{
-			break;
-		}
-	}
-	if(primarySkill >= GameConstants::PRIMARY_SKILLS)
+	if (isCampaignYog())
 	{
-		primarySkill = rand.nextInt(GameConstants::PRIMARY_SKILLS - 1);
-		logGlobal->error("Wrong values in primarySkill%sLevel for hero class %s", isLowLevelHero ? "Low" : "High", type->heroClass->getNameTranslated());
-		randomValue = 100 / GameConstants::PRIMARY_SKILLS;
+		// Yog can only receive Attack or Defence on level-up
+		std::vector<int> yogChances = { skillChances[0], skillChances[1]};
+		return static_cast<PrimarySkill>(RandomGeneratorUtil::nextItemWeighted(yogChances, rand));
 	}
-	logGlobal->trace("The hero gets the primary skill %d with a probability of %d %%.", primarySkill, randomValue);
-	return static_cast<PrimarySkill>(primarySkill);
+
+	return static_cast<PrimarySkill>(RandomGeneratorUtil::nextItemWeighted(skillChances, rand));
 }
 
 std::optional<SecondarySkill> CGHeroInstance::nextSecondarySkill(CRandomGenerator & rand) const
@@ -1773,6 +1777,12 @@ bool CGHeroInstance::isMissionCritical() const
 			if ((condition.condition == EventCondition::CONTROL) && condition.objectID != ObjectInstanceID::NONE)
 				return (id != condition.objectID);
 
+			if (condition.condition == EventCondition::HAVE_ARTIFACT)
+			{
+				if(hasArt(condition.objectType.as<ArtifactID>()))
+					return true;
+			}
+
 			if(condition.condition == EventCondition::IS_HUMAN)
 				return true;
 
@@ -1799,4 +1809,40 @@ void CGHeroInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &s
 	}
 }
 
+bool CGHeroInstance::isCampaignYog() const
+{
+	const StartInfo *si = cb->getStartInfo();
+
+	// it would be nice to find a way to move this hack to config/mapOverrides.json
+	if(!si || !si->campState)
+		return false;
+
+	std::string campaign = si->campState->getFilename();
+	if (!boost::starts_with(campaign, "DATA/YOG")) // "Birth of a Barbarian"
+		return false;
+
+	if (getHeroType() != HeroTypeID::SOLMYR) // Yog (based on Solmyr)
+		return false;
+
+	return true;
+}
+
+bool CGHeroInstance::isCampaignGem() const
+{
+	const StartInfo *si = cb->getStartInfo();
+
+	// it would be nice to find a way to move this hack to config/mapOverrides.json
+	if(!si || !si->campState)
+		return false;
+
+	std::string campaign = si->campState->getFilename();
+	if (!boost::starts_with(campaign, "DATA/GEM") &&  !boost::starts_with(campaign, "DATA/FINAL")) // "New Beginning" and "Unholy Alliance"
+		return false;
+
+	if (getHeroType() != HeroTypeID::GEM) // Yog (based on Solmyr)
+		return false;
+
+	return true;
+}
+
 VCMI_LIB_NAMESPACE_END

+ 7 - 0
lib/mapObjects/CGHeroInstance.h

@@ -149,6 +149,9 @@ public:
 	HeroTypeID getPortraitSource() const;
 	int32_t getIconIndex() const;
 
+	std::string getClassNameTranslated() const;
+	std::string getClassNameTextID() const;
+
 private:
 	std::string getNameTextID() const;
 	std::string getBiographyTextID() const;
@@ -305,6 +308,10 @@ public:
 	bool isCoastVisitable() const override;
 	bool isBlockedVisitable() const override;
 	BattleField getBattlefield() const override;
+
+	bool isCampaignYog() const;
+	bool isCampaignGem() const;
+
 protected:
 	void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;//synchr
 	///common part of hero instance and hero definition

+ 7 - 6
lib/mapObjects/CQuest.cpp

@@ -31,6 +31,7 @@
 #include "../modding/ModUtility.h"
 #include "../networkPacks/PacksForClient.h"
 #include "../spells/CSpellHandler.h"
+#include "../CRandomGenerator.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -128,12 +129,12 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const
 	if(!mission.heroAllowed(h))
 		return false;
 	
-	if(killTarget != ObjectInstanceID::NONE)
+	if(killTarget.hasValue())
 	{
-		if(h->cb->getObjByQuestIdentifier(killTarget))
+		PlayerColor owner = h->getOwner();
+		if (!h->cb->getPlayerState(owner)->destroyedObjects.count(killTarget))
 			return false;
 	}
-	
 	return true;
 }
 
@@ -611,7 +612,7 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const
 
 int CGSeerHut::checkDirection() const
 {
-	int3 cord = getCreatureToKill()->pos;
+	int3 cord = getCreatureToKill(false)->pos;
 	if(static_cast<double>(cord.x) / static_cast<double>(cb->getMapSize().x) < 0.34) //north
 	{
 		if(static_cast<double>(cord.y) / static_cast<double>(cb->getMapSize().y) < 0.34) //northwest
@@ -643,7 +644,7 @@ int CGSeerHut::checkDirection() const
 
 const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const
 {
-	const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget);
+	const CGObjectInstance *o = cb->getObj(quest->killTarget);
 	if(allowNull && !o)
 		return nullptr;
 	return dynamic_cast<const CGHeroInstance *>(o);
@@ -651,7 +652,7 @@ const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const
 
 const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const
 {
-	const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget);
+	const CGObjectInstance *o = cb->getObj(quest->killTarget);
 	if(allowNull && !o)
 		return nullptr;
 	return dynamic_cast<const CGCreature *>(o);

+ 2 - 2
lib/mapObjects/CQuest.h

@@ -136,8 +136,8 @@ public:
 	virtual void init(CRandomGenerator & rand);
 	int checkDirection() const; //calculates the region of map where monster is placed
 	void setObjToKill(); //remember creatures / heroes to kill after they are initialized
-	const CGHeroInstance *getHeroToKill(bool allowNull = false) const;
-	const CGCreature *getCreatureToKill(bool allowNull = false) const;
+	const CGHeroInstance *getHeroToKill(bool allowNull) const;
+	const CGCreature *getCreatureToKill(bool allowNull) const;
 	void getRolloverText (MetaString &text, bool onHover) const;
 
 	void afterAddToMap(CMap * map) override;

+ 11 - 0
lib/mapping/CMap.cpp

@@ -684,4 +684,15 @@ void CMap::resetStaticData()
 	townUniversitySkills.clear();
 }
 
+void CMap::resolveQuestIdentifiers()
+{
+	//FIXME: move to CMapLoaderH3M
+	for (auto & quest : quests)
+	{
+		if (quest->killTarget != ObjectInstanceID::NONE)
+			quest->killTarget = questIdentifierToId[quest->killTarget.getNum()];
+	}
+	questIdentifierToId.clear();
+}
+
 VCMI_LIB_NAMESPACE_END

+ 9 - 1
lib/mapping/CMap.h

@@ -126,6 +126,7 @@ public:
 	void checkForObjectives();
 
 	void resetStaticData();
+	void resolveQuestIdentifiers();
 
 	ui32 checksum;
 	std::vector<Rumor> rumors;
@@ -186,7 +187,14 @@ public:
 		h & artInstances;
 		h & quests;
 		h & allHeroes;
-		h & questIdentifierToId;
+
+		if (h.version < Handler::Version::DESTROYED_OBJECTS)
+		{
+			// old save compatibility
+			//FIXME: remove this field after save-breaking change
+			h & questIdentifierToId;
+			resolveQuestIdentifiers();
+		}
 
 		//TODO: viccondetails
 		h & terrain;

+ 2 - 0
lib/mapping/MapFormatH3M.cpp

@@ -2392,6 +2392,8 @@ void CMapLoaderH3M::afterRead()
 			p.posOfMainTown = posOfMainTown + mainTown->getVisitableOffset();
 		}
 	}
+
+	map->resolveQuestIdentifiers();
 }
 
 VCMI_LIB_NAMESPACE_END

+ 3 - 21
lib/networkPacks/NetPacksLib.cpp

@@ -1156,6 +1156,9 @@ void RemoveObject::applyGs(CGameState *gs)
 	//unblock tiles
 	gs->map->removeBlockVisTiles(obj);
 
+	if (initiator.isValidPlayer())
+		gs->getPlayerState(initiator)->destroyedObjects.insert(objectID);
+
 	if(obj->ID == Obj::HERO) //remove beaten hero
 	{
 		auto * beatenHero = dynamic_cast<CGHeroInstance *>(obj);
@@ -1221,27 +1224,6 @@ void RemoveObject::applyGs(CGameState *gs)
 		}
 	}
 
-	for (TriggeredEvent & event : gs->map->triggeredEvents)
-	{
-		auto patcher = [&](EventCondition cond) -> EventExpression::Variant
-		{
-			if (cond.objectID == obj->id)
-			{
-				if (cond.condition == EventCondition::DESTROY)
-				{
-					cond.condition = EventCondition::CONST_VALUE;
-					cond.value = 1; // destroyed object, from now on always fulfilled
-				}
-				else if (cond.condition == EventCondition::CONTROL)
-				{
-					cond.condition = EventCondition::CONST_VALUE;
-					cond.value = 0; // destroyed object, from now on can not be fulfilled
-				}
-			}
-			return cond;
-		};
-		event.trigger = event.trigger.morph(patcher);
-	}
 	gs->map->instanceNames.erase(obj->instanceName);
 	gs->map->objects[objectID.getNum()].dellNull();
 	gs->map->calculateGuardingGreaturePositions();

+ 1 - 0
lib/rewardable/Info.cpp

@@ -20,6 +20,7 @@
 #include "../JsonRandom.h"
 #include "../mapObjects/IObjectInterface.h"
 #include "../modding/IdentifierStorage.h"
+#include "../CRandomGenerator.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 3 - 1
lib/serializer/ESerializationVersion.h

@@ -34,6 +34,8 @@ enum class ESerializationVersion : int32_t
 	MINIMAL = 831,
 	RELEASE_143, // 832 +text container in campaigns, +starting hero in RMG options
 	HAS_EXTRA_OPTIONS, // 833 +extra options struct as part of startinfo
+	DESTROYED_OBJECTS, // 834 +list of objects destroyed by player
+	CAMPAIGN_MAP_TRANSLATIONS,
 
-	CURRENT = HAS_EXTRA_OPTIONS
+	CURRENT = CAMPAIGN_MAP_TRANSLATIONS
 };

+ 1 - 0
lib/spells/BattleSpellMechanics.cpp

@@ -19,6 +19,7 @@
 #include "../networkPacks/PacksForClientBattle.h"
 #include "../networkPacks/SetStackEffect.h"
 #include "../CStack.h"
+#include "../CRandomGenerator.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 1 - 0
lib/spells/effects/Catapult.cpp

@@ -20,6 +20,7 @@
 #include "../../mapObjects/CGTownInstance.h"
 #include "../../networkPacks/PacksForClientBattle.h"
 #include "../../serializer/JsonSerializeFormat.h"
+#include "../../CRandomGenerator.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 2 - 0
server/NetPacksLobbyServer.cpp

@@ -15,6 +15,8 @@
 
 #include "../lib/serializer/Connection.h"
 #include "../lib/StartInfo.h"
+#include "../lib/CRandomGenerator.h"
+
 
 // Campaigns
 #include "../lib/campaign/CampaignState.h"