Browse Source

Merge remote-tracking branch 'vcmi/develop' into lobby

Ivan Savenko 1 year ago
parent
commit
80fc2bb695
100 changed files with 1244 additions and 647 deletions
  1. 3 56
      .github/workflows/github.yml
  2. 2 0
      AI/BattleAI/BattleEvaluator.cpp
  3. 0 9
      AI/Nullkiller/AIGateway.cpp
  4. 1 0
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  5. 1 1
      AI/Nullkiller/Goals/CompleteQuest.cpp
  6. 1 0
      AI/StupidAI/StupidAI.cpp
  7. 19 20
      AI/VCAI/BuildingManager.cpp
  8. 1 1
      AI/VCAI/Goals/CompleteQuest.cpp
  9. 0 9
      AI/VCAI/VCAI.cpp
  10. 46 5
      ChangeLog.md
  11. 14 9
      Mods/vcmi/config/vcmi/english.json
  12. 11 7
      Mods/vcmi/config/vcmi/german.json
  13. 3 3
      client/CMT.cpp
  14. 29 0
      client/CMusicHandler.cpp
  15. 1 0
      client/CMusicHandler.h
  16. 39 5
      client/CPlayerInterface.cpp
  17. 1 0
      client/CPlayerInterface.h
  18. 31 7
      client/CVideoHandler.cpp
  19. 9 3
      client/CVideoHandler.h
  20. 18 6
      client/Client.cpp
  21. 10 6
      client/adventureMap/AdventureOptions.cpp
  22. 1 1
      client/adventureMap/AdventureOptions.h
  23. 1 1
      client/adventureMap/CList.cpp
  24. 1 0
      client/battle/BattleActionsController.cpp
  25. 1 3
      client/battle/BattleInterfaceClasses.cpp
  26. 1 1
      client/battle/BattleInterfaceClasses.h
  27. 1 0
      client/battle/BattleStacksController.cpp
  28. 92 17
      client/battle/BattleWindow.cpp
  29. 8 0
      client/battle/BattleWindow.h
  30. 1 0
      client/gui/Shortcut.h
  31. 2 0
      client/gui/ShortcutHandler.cpp
  32. 1 1
      client/lobby/CSelectionBase.cpp
  33. 24 12
      client/mainmenu/CPrologEpilogVideo.cpp
  34. 3 0
      client/mainmenu/CPrologEpilogVideo.h
  35. 0 80
      client/widgets/CArtifactsOfHeroAltar.cpp
  36. 0 12
      client/widgets/CArtifactsOfHeroAltar.h
  37. 4 15
      client/widgets/CArtifactsOfHeroBackpack.cpp
  38. 0 2
      client/widgets/CArtifactsOfHeroBackpack.h
  39. 9 13
      client/widgets/CArtifactsOfHeroBase.cpp
  40. 1 2
      client/widgets/CArtifactsOfHeroBase.h
  41. 0 12
      client/widgets/CArtifactsOfHeroKingdom.cpp
  42. 2 3
      client/widgets/CArtifactsOfHeroKingdom.h
  43. 0 11
      client/widgets/CArtifactsOfHeroMain.cpp
  44. 0 2
      client/widgets/CArtifactsOfHeroMain.h
  45. 2 3
      client/widgets/CArtifactsOfHeroMarket.cpp
  46. 50 41
      client/widgets/CWindowWithArtifacts.cpp
  47. 1 0
      client/widgets/CWindowWithArtifacts.h
  48. 101 99
      client/widgets/markets/CAltarArtifacts.cpp
  49. 9 4
      client/widgets/markets/CAltarArtifacts.h
  50. 10 2
      client/windows/CAltarWindow.cpp
  51. 18 8
      client/windows/CCastleInterface.cpp
  52. 3 3
      client/windows/CHeroWindow.cpp
  53. 2 2
      client/windows/CSpellWindow.cpp
  54. 24 10
      client/windows/GUIClasses.cpp
  55. 8 2
      client/windows/GUIClasses.h
  56. 12 0
      client/windows/settings/BattleOptionsTab.cpp
  57. 1 0
      client/windows/settings/BattleOptionsTab.h
  58. 211 0
      config/mapOverrides.json
  59. 5 1
      config/schemas/settings.json
  60. 17 0
      config/widgets/settings/battleOptionsTab.json
  61. 2 2
      docs/Readme.md
  62. 19 0
      include/vstd/RNG.h
  63. 9 4
      lib/ArtifactUtils.cpp
  64. 8 1
      lib/CArtHandler.cpp
  65. 2 1
      lib/CArtHandler.h
  66. 1 0
      lib/CCreatureHandler.cpp
  67. 1 1
      lib/CCreatureHandler.h
  68. 2 27
      lib/CGameInfoCallback.cpp
  69. 1 3
      lib/CGameInfoCallback.h
  70. 1 1
      lib/CGeneralTextHandler.h
  71. 5 4
      lib/CHeroHandler.cpp
  72. 6 0
      lib/CPlayerState.h
  73. 28 0
      lib/IGameCallback.cpp
  74. 3 1
      lib/IGameCallback.h
  75. 5 0
      lib/MetaString.cpp
  76. 1 0
      lib/MetaString.h
  77. 2 0
      lib/battle/BattleInfo.cpp
  78. 1 0
      lib/battle/CBattleInfoCallback.cpp
  79. 22 9
      lib/campaign/CampaignHandler.cpp
  80. 1 1
      lib/campaign/CampaignHandler.h
  81. 5 2
      lib/campaign/CampaignState.cpp
  82. 6 1
      lib/campaign/CampaignState.h
  83. 3 0
      lib/constants/EntityIdentifiers.cpp
  84. 7 1
      lib/constants/EntityIdentifiers.h
  85. 1 0
      lib/constants/NumericConstants.h
  86. 2 2
      lib/filesystem/CCompressedStream.cpp
  87. 6 0
      lib/filesystem/CCompressedStream.h
  88. 1 4
      lib/gameState/CGameState.cpp
  89. 1 0
      lib/gameState/CGameState.h
  90. 107 41
      lib/gameState/CGameStateCampaign.cpp
  91. 10 4
      lib/gameState/CGameStateCampaign.h
  92. 1 0
      lib/mapObjectConstructors/CBankInstanceConstructor.cpp
  93. 5 0
      lib/mapObjectConstructors/CommonConstructors.cpp
  94. 1 0
      lib/mapObjects/CGCreature.cpp
  95. 65 19
      lib/mapObjects/CGHeroInstance.cpp
  96. 7 0
      lib/mapObjects/CGHeroInstance.h
  97. 5 0
      lib/mapObjects/CGMarket.cpp
  98. 15 0
      lib/mapObjects/CGMarket.h
  99. 7 6
      lib/mapObjects/CQuest.cpp
  100. 2 2
      lib/mapObjects/CQuest.h

+ 3 - 56
.github/workflows/github.yml

@@ -6,9 +6,8 @@ on:
       - features/*
       - beta
       - master
+      - develop
   pull_request:
-  schedule:
-    - cron: '0 2 * * *'
   workflow_dispatch:
 
 env:
@@ -16,59 +15,7 @@ env:
   BUILD_TYPE: Release
 
 jobs:
-  check_last_build:
-    if: github.event.schedule != ''
-    runs-on: ubuntu-latest
-    outputs:
-      skip_build: ${{ steps.check_if_built.outputs.skip_build }}
-    defaults:
-      run:
-        shell: bash
-    steps:
-      - name: Get repo name
-        id: get_repo_name
-        run: echo "::set-output name=value::${GITHUB_REPOSITORY#*/}"
-
-      - name: Get last successful build for ${{ github.sha }}
-        uses: octokit/[email protected]
-        id: get_last_scheduled_run
-        with:
-          route: GET /repos/{owner}/{repo}/actions/runs
-          owner: ${{ github.repository_owner }}
-          repo: ${{ steps.get_repo_name.outputs.value }}
-          status: success
-          per_page: 1
-          head_sha: ${{ github.sha }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Check if successful build of the current commit exists
-        id: check_if_built
-        run: |
-          if [ ${{ fromJson(steps.get_last_scheduled_run.outputs.data).total_count }} -gt 0 ]; then
-            echo '::set-output name=skip_build::1'
-          else
-            echo '::set-output name=skip_build::0'
-          fi
-
-      - name: Cancel current run
-        if: steps.check_if_built.outputs.skip_build == 1
-        uses: octokit/[email protected]
-        with:
-          route: POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel
-          owner: ${{ github.repository_owner }}
-          repo: ${{ steps.get_repo_name.outputs.value }}
-          run_id: ${{ github.run_id }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Wait for the run to be cancelled
-        if: steps.check_if_built.outputs.skip_build == 1
-        run: sleep 60
-
   build:
-    needs: check_last_build
-    if: always() && needs.check_last_build.skip_build != 1
     strategy:
       matrix:
         include:
@@ -199,9 +146,9 @@ jobs:
         max-size: "5G"
         verbose: 2
 
-    - name: Ccache for everything but PRs
+    - name: Ccache for vcmi/vcmi's develop branch
       uses: hendrikmuhs/[email protected]
-      if: ${{ github.event.number == '' }}
+      if: ${{ github.event.number == '' && github.ref == 'refs/heads/develop' }}
       with:
         key: ${{ matrix.preset }}-no-PR
         restore-keys: |

+ 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,

+ 0 - 9
AI/Nullkiller/AIGateway.cpp

@@ -1131,15 +1131,6 @@ void AIGateway::battleEnd(const BattleID & battleID, const BattleResult * br, Qu
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
 
-	if (queryID != QueryID::NONE)
-	{
-		status.addQuery(queryID, "Combat result dialog");
-		const int confirmAction = 0;
-		requestActionASAP([=]()
-		{
-			answerQuery(queryID, confirmAction);
-		});
-	}
 	CAdventureAI::battleEnd(battleID, br, queryID);
 }
 

+ 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)

+ 19 - 20
AI/VCAI/BuildingManager.cpp

@@ -139,17 +139,17 @@ void BuildingManager::setAI(VCAI * AI)
 //Set of buildings for different goals. Does not include any prerequisites.
 static const std::vector<BuildingID> essential = { BuildingID::TAVERN, BuildingID::TOWN_HALL };
 static const std::vector<BuildingID> basicGoldSource = { BuildingID::TOWN_HALL, BuildingID::CITY_HALL };
+static const std::vector<BuildingID> defence = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE };
 static const std::vector<BuildingID> capitolAndRequirements = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::CAPITOL };
 static const std::vector<BuildingID> unitsSource = { BuildingID::DWELL_LVL_1, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3,
 BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7 };
 static const std::vector<BuildingID> unitsUpgrade = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP,
 BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP };
-static const std::vector<BuildingID> unitGrowth = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::HORDE_1,
-BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR };
+static const std::vector<BuildingID> unitGrowth = { BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR };
 static const std::vector<BuildingID> _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3,
 BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 };
-static const std::vector<BuildingID> extra = { BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3,
-BuildingID::SPECIAL_4, BuildingID::SHIPYARD }; // all remaining buildings
+static const std::vector<BuildingID> extra = { BuildingID::MARKETPLACE, BuildingID::BLACKSMITH, BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, 
+BuildingID::SPECIAL_3, BuildingID::SPECIAL_4, BuildingID::SHIPYARD }; // all remaining buildings
 
 bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
 {
@@ -172,33 +172,32 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
 	if(tryBuildAnyStructure(t, essential))
 		return true;
 
-	//the more gold the better and less problems later //TODO: what about building mage guild / marketplace etc. with city hall disabled in editor?
-	if(tryBuildNextStructure(t, basicGoldSource))
-		return true;
-
-	//workaround for mantis #2696 - build capitol with separate algorithm if it is available
-	if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL)
+	if (cb->getDate(Date::DAY_OF_WEEK) < 5) // first 4 days of week - try to focus on dwellings
 	{
-		if(tryBuildNextStructure(t, capitolAndRequirements))
+		if (tryBuildNextStructure(t, unitsSource, 4))
 			return true;
 	}
 
-	if(!t->hasBuilt(BuildingID::FORT)) //in vast majority of situations fort is top priority building if we already have city hall, TODO: unite with unitGrowth building chain
-		if(tryBuildThisStructure(t, BuildingID::FORT))
+	if (cb->getDate(Date::DAY_OF_WEEK) > 4) // last 3 days of week - try to focus on growth by building Fort/Citadel/Castle
+	{
+		if (tryBuildNextStructure(t, defence, 3))
 			return true;
+	}
 
-
-
-	if (cb->getDate(Date::DAY_OF_WEEK) > 6) // last 2 days of week - try to focus on growth
+	if (t->hasBuilt(BuildingID::CASTLE))
 	{
-		if (tryBuildNextStructure(t, unitGrowth, 2))
+		if (tryBuildAnyStructure(t, unitGrowth))
 			return true;
 	}
 
-	//try building dwellings
-	if (t->hasBuilt(BuildingID::FORT))
+	//try to make City Hall
+	if (tryBuildNextStructure(t, basicGoldSource))
+		return true;
+
+	//workaround for mantis #2696 - build capitol with separate algorithm if it is available
+	if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL)
 	{
-		if (tryBuildAnyStructure(t, unitsSource, 8 - cb->getDate(Date::DAY_OF_WEEK)))
+		if(tryBuildNextStructure(t, capitolAndRequirements))
 			return true;
 	}
 

+ 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);

+ 0 - 9
AI/VCAI/VCAI.cpp

@@ -1608,15 +1608,6 @@ void VCAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
 
-	if (queryID != QueryID::NONE)
-	{
-		status.addQuery(queryID, "Combat result dialog");
-		const int confirmAction = 0;
-		requestActionASAP([=]()
-		{
-			answerQuery(queryID, confirmAction);
-		});
-	}
 	CAdventureAI::battleEnd(battleID, br, queryID);
 }
 

+ 46 - 5
ChangeLog.md

@@ -1,14 +1,55 @@
 # 1.4.5 -> 1.5.0
 
 ### General
-* Added Chinese translation to map editor
-* Added Spanish translation to launcher
-* Fixed reversed Overlord and Warlock classes mapping
 * Added option to disable cheats in game
-* Added option for unlimited combat replays
-* Fixed assembly of artifacts in the backpack when backpack is full
+
+### Interface
+* Town Portal dialog will now show town icons
+* Town Portal dialog will now show town info on right click
+* Town Portal dialog will center on town on clicking it
+* Town Portal dialog now uses same town ordering as in adventure map interface
+* Heroes can now be recruited from the tavern by double-clicking on them
 * Added status bar to the backpack window
 * Quick backpack window is now only available when enabled Interface enhancements
+* Fixed assembly of artifacts in the backpack when backpack is full
+* Attempt to use enemy turn replay feature will now show "Not implemented" message
+
+### Campaigns
+* Game will now correctly track who defeated the hero or wandering monsters for related quests and victory conditions
+* Birth of a Barbarian: Yog will now start the third scenario with Angelic Alliance in his inventory
+* Birth of a Barbarian: Heroes with Angelic Alliance components are now considered to be mission-critical and can't be dismissed or lost in combat
+* Birth of a Barbarian: Yog can no longer purchase spellbook from the Mage Guild
+* Birth of a Barbarian: Yog will no longer gain Spellpower or Knowledge when leveling up
+* Birth of a Barbarian: Scenarios with mission to deliver an artifact will no longer end after just defeating enemies
+* Gem will now have her class set to "Sorceress" in campaigns
+* Fixed missing names for heroes who have their names customized in map after being transferred to the next scenario
+* Artifact transfer will now work correctly if the hero holding the transferable artifact is not also transferring
+* Fixed crash on opening of some campaigns in the French version from gog.com
+* It is now possible to replay the intro movie from the scenario information window
+* When playing the intro video, the subtitles are now correctly synchronized with the audio
+
+### Battles
+* Added option to enable unlimited combat replays during game setup
+* Added option to instantly end battle using quick combat (shotcut: 'e')
+* Added option to replace auto-combat button action with instant end using quick combat
+* Battles against AI players can now be done using quick combat
+* Disabling battle queue will now correctly reposition hero statistics preview popup
+* Fixed positioning of unit stack size label
+
+### Launcher
+* Added Spanish translation to launcher
+
+### Map Editor
+* Added Chinese translation to map editor
+
+### AI
+* Fixed possible crash on updating NKAI pathfinding data
+* Fixed counting mana usage cost of Fly spell
+* Added estimation of value of Pyramid and Cyclops Stockpile
+
+### Modding
+* Added new game setting that allows inviting heroes to taverns
+* Fixed reversed Overlord and Warlock classes mapping
 
 # 1.4.4 -> 1.4.5
 

+ 14 - 9
Mods/vcmi/config/vcmi/english.json

@@ -13,13 +13,14 @@
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Deadly",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Impossible",
 
-	"vcmi.adventureMap.confirmRestartGame"     : "Are you sure you want to restart the game?",
-	"vcmi.adventureMap.noTownWithMarket"       : "There are no available marketplaces!",
-	"vcmi.adventureMap.noTownWithTavern"       : "There are no available towns with taverns!",
-	"vcmi.adventureMap.spellUnknownProblem"    : "There is an unknown problem with this spell! No more information is available.",
-	"vcmi.adventureMap.playerAttacked"         : "Player has been attacked: %s",
-	"vcmi.adventureMap.moveCostDetails"        : "Movement points - Cost: %TURNS turns + %POINTS points, Remaining points: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Movement points - Cost: %POINTS points, Remaining points: %REMAINING",
+	"vcmi.adventureMap.confirmRestartGame"               : "Are you sure you want to restart the game?",
+	"vcmi.adventureMap.noTownWithMarket"                 : "There are no available marketplaces!",
+	"vcmi.adventureMap.noTownWithTavern"                 : "There are no available towns with taverns!",
+	"vcmi.adventureMap.spellUnknownProblem"              : "There is an unknown problem with this spell! No more information is available.",
+	"vcmi.adventureMap.playerAttacked"                   : "Player has been attacked: %s",
+	"vcmi.adventureMap.moveCostDetails"                  : "Movement points - Cost: %TURNS turns + %POINTS points, Remaining points: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Movement points - Cost: %POINTS points, Remaining points: %REMAINING",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sorry, replay opponent turn is not implemented yet!",
 
 	"vcmi.capitalColors.0" : "Red",
 	"vcmi.capitalColors.1" : "Blue",
@@ -190,8 +191,10 @@
 	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Show heroes statistics windows",
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Show heroes statistics windows}\n\nPermanently toggle on heroes statistics windows that show primary stats and spell points.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music",
-	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.",
-	
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.",	
+	"vcmi.battleOptions.endWithAutocombat.hover": "Ends battle",
+	"vcmi.battleOptions.endWithAutocombat.help": "{Ends battle}\n\nAuto-Combat plays battle to end instant",
+
 	"vcmi.adventureMap.revisitObject.hover" : "Revisit Object",
 	"vcmi.adventureMap.revisitObject.help" : "{Revisit Object}\n\nIf a hero currently stands on a Map Object, he can revisit the location.",
 
@@ -210,6 +213,7 @@
 	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s were killed by accurate shots!",
 	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s was killed with an accurate shot!",
 	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s were killed by accurate shots!",
+	"vcmi.battleWindow.endWithAutocombat" : "Are you sure you wish to end the battle with auto combat?",
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result",
 
@@ -350,6 +354,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",

+ 11 - 7
Mods/vcmi/config/vcmi/german.json

@@ -13,13 +13,14 @@
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich",
 
-	"vcmi.adventureMap.confirmRestartGame"     : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?",
-	"vcmi.adventureMap.noTownWithMarket"       : "Kein Marktplatz verfügbar!",
-	"vcmi.adventureMap.noTownWithTavern"       : "Keine Stadt mit Taverne verfügbar!",
-	"vcmi.adventureMap.spellUnknownProblem"    : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.",
-	"vcmi.adventureMap.playerAttacked"         : "Spieler wurde attackiert: %s",
-	"vcmi.adventureMap.moveCostDetails"        : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING",
+	"vcmi.adventureMap.confirmRestartGame"               : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?",
+	"vcmi.adventureMap.noTownWithMarket"                 : "Kein Marktplatz verfügbar!",
+	"vcmi.adventureMap.noTownWithTavern"                 : "Keine Stadt mit Taverne verfügbar!",
+	"vcmi.adventureMap.spellUnknownProblem"              : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.",
+	"vcmi.adventureMap.playerAttacked"                   : "Spieler wurde attackiert: %s",
+	"vcmi.adventureMap.moveCostDetails"                  : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Das Wiederholen des gegnerischen Zuges ist aktuell noch nicht implementiert!",
 
 	"vcmi.capitalColors.0" : "Rot",
 	"vcmi.capitalColors.1" : "Blau",
@@ -171,6 +172,8 @@
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.",
+	"vcmi.battleOptions.endWithAutocombat.hover": "Kampf beenden",
+	"vcmi.battleOptions.endWithAutocombat.help": "{Kampf beenden}\n\nAutokampf spielt den Kampf sofort zu Ende",
 	
 	"vcmi.adventureMap.revisitObject.hover" : "Objekt erneut besuchen",
 	"vcmi.adventureMap.revisitObject.help" : "{Objekt erneut besuchen}\n\nSteht ein Held gerade auf einem Kartenobjekt, kann er den Ort erneut aufsuchen.",
@@ -190,6 +193,7 @@
 	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s wurden durch gezielte Schüsse getötet!",
 	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s wurde mit einem gezielten Schuss getötet!",
 	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s wurden durch gezielte Schüsse getötet!",
+	"vcmi.battleWindow.endWithAutocombat" : "Seid Ihr sicher, dass Ihr den Kampf mit Auto-Kampf beenden wollt?",
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen",
 

+ 3 - 3
client/CMT.cpp

@@ -381,15 +381,15 @@ void playIntro()
 {
 	auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK"));
 	int sound = CCS->soundh->playSound(audioData);
-	if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, true, true))
+	if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, EVideoType::INTRO))
 	{
 		audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK"));
 		sound = CCS->soundh->playSound(audioData);
-		if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true))
+		if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, EVideoType::INTRO))
 		{
 			audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK"));
 			sound = CCS->soundh->playSound(audioData);
-			CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, true, true);
+			CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, EVideoType::INTRO);
 		}
 	}
 	CCS->soundh->stopSound(sound);

+ 29 - 0
client/CMusicHandler.cpp

@@ -183,6 +183,35 @@ void CSoundHandler::ambientStopSound(const AudioPath & soundId)
 	setChannelVolume(ambientChannels[soundId], volume);
 }
 
+uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound)
+{
+	if (!initialized || sound.empty())
+		return 0;
+
+	auto resourcePath = sound.addPrefix("SOUNDS/");
+
+	if (!CResourceHandler::get()->existsResource(resourcePath))
+		return 0;
+
+	auto data = CResourceHandler::get()->load(resourcePath)->readAll();
+
+	SDL_AudioSpec spec;
+	uint32_t audioLen;
+	uint8_t *audioBuf;
+	uint32_t miliseconds = 0;
+
+	if(SDL_LoadWAV_RW(SDL_RWFromMem(data.first.get(), (int)data.second), 1, &spec, &audioBuf, &audioLen) != nullptr)
+	{
+		SDL_FreeWAV(audioBuf);
+		uint32_t sampleSize = SDL_AUDIO_BITSIZE(spec.format) / 8;
+		uint32_t sampleCount = audioLen / sampleSize;
+		uint32_t sampleLen = sampleCount / spec.channels;
+		miliseconds = 1000 * sampleLen / spec.freq;
+	}
+
+	return miliseconds ;
+}
+
 // Plays a sound, and return its channel so we can fade it out later
 int CSoundHandler::playSound(soundBase::soundID soundID, int repeats)
 {

+ 1 - 0
client/CMusicHandler.h

@@ -77,6 +77,7 @@ public:
 	void setChannelVolume(int channel, ui32 percent);
 
 	// Sounds
+	uint32_t getSoundDurationMilliseconds(const AudioPath & sound);
 	int playSound(soundBase::soundID soundID, int repeats=0);
 	int playSound(const AudioPath & sound, int repeats=0, bool cache=false);
 	int playSound(std::pair<std::unique_ptr<ui8 []>, si64> & data, int repeats=0, bool cache=false);

+ 39 - 5
client/CPlayerInterface.cpp

@@ -43,6 +43,7 @@
 
 #include "render/CAnimation.h"
 #include "render/IImage.h"
+#include "render/IRenderHandler.h"
 
 #include "widgets/Buttons.h"
 #include "widgets/CComponent.h"
@@ -145,6 +146,7 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player):
 	firstCall = 1; //if loading will be overwritten in serialize
 	autosaveCount = 0;
 	isAutoFightOn = false;
+	isAutoFightEndBattle = false;
 	ignoreEvents = false;
 	numOfMovedArts = 0;
 }
@@ -782,17 +784,20 @@ void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult *
 
 		if(!battleInt)
 		{
-			bool allowManualReplay = queryID != QueryID::NONE;
+			bool allowManualReplay = queryID != QueryID::NONE && !isAutoFightEndBattle;
 
 			auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay);
 
-			if (allowManualReplay)
+			if (allowManualReplay || isAutoFightEndBattle)
 			{
 				wnd->resultCallback = [=](ui32 selection)
 				{
 					cb->selectionMade(selection, queryID);
 				};
 			}
+			
+			isAutoFightEndBattle = false;
+
 			GH.windows().pushWindow(wnd);
 			// #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it.
 			// Otherwise NewTurn causes freeze.
@@ -1064,6 +1069,19 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 
+	std::vector<ObjectInstanceID> tmpObjects;
+	if(objects.size() && dynamic_cast<const CGTownInstance *>(cb->getObj(objects[0])))
+	{
+		// sorting towns (like in client)
+		std::vector <const CGTownInstance*> Towns = LOCPLINT->localState->getOwnedTowns();
+		for(auto town : Towns)
+			for(auto item : objects)
+				if(town == cb->getObj(item))
+					tmpObjects.push_back(item);
+	}
+	else // other object list than town
+		tmpObjects = objects;
+
 	auto selectCallback = [=](int selection)
 	{
 		cb->sendQueryReply(selection, askID);
@@ -1078,9 +1096,9 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 	const std::string localDescription = description.toString();
 
 	std::vector<int> tempList;
-	tempList.reserve(objects.size());
+	tempList.reserve(tmpObjects.size());
 
-	for(auto item : objects)
+	for(auto item : tmpObjects)
 		tempList.push_back(item.getNum());
 
 	CComponent localIconC(icon);
@@ -1088,8 +1106,24 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 	std::shared_ptr<CIntObject> localIcon = localIconC.image;
 	localIconC.removeChild(localIcon.get(), false);
 
-	auto wnd = std::make_shared<CObjectListWindow>(tempList, localIcon, localTitle, localDescription, selectCallback);
+	std::vector<std::shared_ptr<IImage>> images;
+	for(auto & obj : tmpObjects)
+	{
+		if(!settings["general"]["enableUiEnhancements"].Bool())
+			break;
+		const CGTownInstance * t = dynamic_cast<const CGTownInstance *>(cb->getObj(obj));
+		if(t)
+		{
+			std::shared_ptr<CAnimation> a = GH.renderHandler().loadAnimation(AnimationPath::builtin("ITPA"));
+			a->preload();
+			images.push_back(a->getImage(t->town->clientInfo.icons[t->hasFort()][false] + 2)->scaleFast(Point(35, 23)));
+		}
+	}
+
+	auto wnd = std::make_shared<CObjectListWindow>(tempList, localIcon, localTitle, localDescription, selectCallback, 0, images);
 	wnd->onExit = cancelCallback;
+	wnd->onPopup = [this, tmpObjects](int index) { CRClickPopup::createAndPush(cb->getObj(tmpObjects[index]), GH.getCursorPosition()); };
+	wnd->onClicked = [this, tmpObjects](int index) { adventureInt->centerOnObject(cb->getObj(tmpObjects[index])); GH.windows().totalRedraw(); };
 	GH.windows().pushWindow(wnd);
 }
 

+ 1 - 0
client/CPlayerInterface.h

@@ -87,6 +87,7 @@ public: // TODO: make private
 	//During battle is quick combat mode is used
 	std::shared_ptr<CBattleGameInterface> autofightingAI; //AI that makes decisions
 	bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface.
+	bool isAutoFightEndBattle; //Flag, if battle forced to end with autocombat
 
 protected: // Call-ins from server, should not be called directly, but only via GameInterface
 

+ 31 - 7
client/CVideoHandler.cpp

@@ -101,8 +101,8 @@ bool CVideoPlayer::open(const VideoPath & fname, bool scale)
 }
 
 // loop = to loop through the video
-// useOverlay = directly write to the screen.
-bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverlay, bool scale)
+// overlay = directly write to the screen.
+bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool overlay, bool scale)
 {
 	close();
 
@@ -199,7 +199,7 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla
 	}
 
 	// Allocate a place to put our YUV image on that screen
-	if (useOverlay)
+	if (overlay)
 	{
 		texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h);
 	}
@@ -624,7 +624,7 @@ Point CVideoPlayer::size()
 }
 
 // Plays a video. Only works for overlays.
-bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
+bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey, bool overlay)
 {
 	// Note: either the windows player or the linux player is
 	// broken. Compensate here until the bug is found.
@@ -647,7 +647,14 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
 
 		SDL_Rect rect = CSDL_Ext::toSDL(pos);
 
-		SDL_RenderFillRect(mainRenderer, &rect);
+		if(overlay)
+		{
+			SDL_RenderFillRect(mainRenderer, &rect);
+		}
+		else
+		{
+			SDL_RenderClear(mainRenderer);
+		}
 		SDL_RenderCopy(mainRenderer, texture, nullptr, &rect);
 		SDL_RenderPresent(mainRenderer);
 
@@ -672,10 +679,27 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
 	return true;
 }
 
-bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey, bool scale)
+bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType)
 {
+	bool scale;
+	bool stopOnKey;
+	bool overlay;
+
+	switch(videoType)
+	{
+		case EVideoType::INTRO:
+			stopOnKey = true;
+			scale = true;
+			overlay = false;
+			break;
+		case EVideoType::SPELLBOOK:
+		default:
+			stopOnKey = false;
+			scale = false;
+			overlay = true;
+	}
 	open(name, false, true, scale);
-	bool ret = playVideo(x, y,  stopOnKey);
+	bool ret = playVideo(x, y,  stopOnKey, overlay);
 	close();
 	return ret;
 }

+ 9 - 3
client/CVideoHandler.h

@@ -15,6 +15,12 @@
 struct SDL_Surface;
 struct SDL_Texture;
 
+enum class EVideoType : ui8
+{
+	INTRO = 0, // use entire window: stopOnKey = true, scale = true, overlay = false
+	SPELLBOOK  // overlay video: stopOnKey = false, scale = false, overlay = true
+};
+
 class IVideoPlayer : boost::noncopyable
 {
 public:
@@ -33,7 +39,7 @@ class IMainVideoPlayer : public IVideoPlayer
 public:
 	virtual ~IMainVideoPlayer() = default;
 	virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function<void()> restart = nullptr){}
-	virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false)
+	virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType)
 	{
 		return false;
 	}
@@ -90,7 +96,7 @@ class CVideoPlayer final : public IMainVideoPlayer
 	double frameTime;
 	bool doLoop;				// loop through video
 
-	bool playVideo(int x, int y, bool stopOnKey);
+	bool playVideo(int x, int y, bool stopOnKey, bool overlay);
 	bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false);
 public:
 	CVideoPlayer();
@@ -106,7 +112,7 @@ public:
 	void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function<void()> onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true
 
 	// Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played)
-	bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override;
+	bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) override;
 
 	std::pair<std::unique_ptr<ui8 []>, si64> getAudio(const VideoPath & videoToOpen) override;
 

+ 18 - 6
client/Client.cpp

@@ -586,14 +586,26 @@ void CClient::battleStarted(const BattleInfo * info)
 		def = std::dynamic_pointer_cast<CPlayerInterface>(playerint[rightSide.color]);
 	
 	//Remove player interfaces for auto battle (quickCombat option)
-	if(att && att->isAutoFightOn)
+	if((att && att->isAutoFightOn) || (def && def->isAutoFightOn))
 	{
-		if (att->cb->getBattle(info->battleID)->battleGetTacticDist())
+		auto endTacticPhaseIfEligible = [info](const CPlayerInterface * interface)
 		{
-			auto side = att->cb->getBattle(info->battleID)->playerToSide(att->playerID);
-			auto action = BattleAction::makeEndOFTacticPhase(*side);
-			att->cb->battleMakeTacticAction(info->battleID, action);
-		}
+			if (interface->cb->getBattle(info->battleID)->battleGetTacticDist())
+			{
+				auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID);
+
+				if(interface->playerID == info->sides[info->tacticsSide].color)
+				{
+					auto action = BattleAction::makeEndOFTacticPhase(*side);
+					interface->cb->battleMakeTacticAction(info->battleID, action);
+				}
+			}
+		};
+
+		if(att && att->isAutoFightOn)
+			endTacticPhaseIfEligible(att.get());
+		else // def && def->isAutoFightOn
+			endTacticPhaseIfEligible(def.get());
 
 		att.reset();
 		def.reset();

+ 10 - 6
client/adventureMap/AdventureOptions.cpp

@@ -23,6 +23,7 @@
 
 #include "../../CCallback.h"
 #include "../../lib/StartInfo.h"
+#include "../../lib/CGeneralTextHandler.h"
 
 AdventureOptions::AdventureOptions()
 	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS"))
@@ -30,12 +31,7 @@ AdventureOptions::AdventureOptions()
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
 	viewWorld = std::make_shared<CButton>(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD);
-	viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); });
-
-	exit = std::make_shared<CButton>(Point(204, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN);
-
-	scenInfo = std::make_shared<CButton>(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
-	scenInfo->addCallback(AdventureOptions::showScenarioInfo);
+	viewWorld->addCallback([] { LOCPLINT->viewWorldMap(); });
 
 	puzzle = std::make_shared<CButton>(Point(24, 81), AnimationPath::builtin("ADVPUZ.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE);
 	puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT));
@@ -45,6 +41,14 @@ AdventureOptions::AdventureOptions()
 		dig->addCallback(std::bind(&CPlayerInterface::tryDigging, LOCPLINT, h));
 	else
 		dig->block(true);
+
+	scenInfo = std::make_shared<CButton>(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
+	scenInfo->addCallback(AdventureOptions::showScenarioInfo);
+	
+	replay = std::make_shared<CButton>(Point(24, 257), AnimationPath::builtin("ADVTURN.DEF"), CButton::tooltip(), [&](){ close(); });
+	replay->addCallback([]{ LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.replayOpponentTurnNotImplemented")); });
+
+	exit = std::make_shared<CButton>(Point(203, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN);
 }
 
 void AdventureOptions::showScenarioInfo()

+ 1 - 1
client/adventureMap/AdventureOptions.h

@@ -21,7 +21,7 @@ class AdventureOptions : public CWindowObject
 	std::shared_ptr<CButton> puzzle;
 	std::shared_ptr<CButton> dig;
 	std::shared_ptr<CButton> scenInfo;
-	/*std::shared_ptr<CButton> replay*/
+	std::shared_ptr<CButton> replay;
 
 public:
 	AdventureOptions();

+ 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 - 3
client/battle/BattleInterfaceClasses.cpp

@@ -449,12 +449,10 @@ void HeroInfoBasicPanel::show(Canvas & to)
 }
 
 
-StackInfoBasicPanel::StackInfoBasicPanel(const CStack * stack, Point * position, bool initializeBackground)
+StackInfoBasicPanel::StackInfoBasicPanel(const CStack * stack, bool initializeBackground)
 	: CIntObject(0)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	if (position != nullptr)
-		moveTo(*position);
 
 	if(initializeBackground)
 	{

+ 1 - 1
client/battle/BattleInterfaceClasses.h

@@ -155,7 +155,7 @@ private:
 	std::vector<std::shared_ptr<CMultiLineLabel>> labelsMultiline;
 	std::vector<std::shared_ptr<CAnimImage>> icons;
 public:
-	StackInfoBasicPanel(const CStack * stack, Point * position, bool initializeBackground = true);
+	StackInfoBasicPanel(const CStack * stack, bool initializeBackground = true);
 
 	void show(Canvas & to) override;
 

+ 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"

+ 92 - 17
client/battle/BattleWindow.cpp

@@ -41,6 +41,8 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/filesystem/ResourcePath.h"
 #include "../../lib/StartInfo.h"
+#include "../../lib/battle/BattleInfo.h"
+#include "../../lib/CPlayerState.h"
 #include "../windows/settings/SettingsMainWindow.h"
 
 BattleWindow::BattleWindow(BattleInterface & owner):
@@ -52,6 +54,12 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	pos.h = 600;
 	pos = center();
 
+	PlayerColor defenderColor = owner.getBattle()->getBattle()->getSidePlayer(BattleSide::DEFENDER);
+	PlayerColor attackerColor = owner.getBattle()->getBattle()->getSidePlayer(BattleSide::ATTACKER);
+	bool isDefenderHuman = defenderColor.isValidPlayer() && LOCPLINT->cb->getStartInfo()->playerInfos.at(defenderColor).isControlledByHuman();
+	bool isAttackerHuman = attackerColor.isValidPlayer() && LOCPLINT->cb->getStartInfo()->playerInfos.at(attackerColor).isControlledByHuman();
+	onlyOnePlayerHuman = isDefenderHuman != isAttackerHuman;
+
 	REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole);
 	
 	const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json"));
@@ -60,6 +68,7 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
 	addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this));
 	addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this));
+	addShortcut(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, std::bind(&BattleWindow::endWithAutocombat, this));
 	addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this));
 	addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this));
 	addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this));
@@ -130,19 +139,13 @@ void BattleWindow::createStickyHeroInfoWindows()
 	{
 		InfoAboutHero info;
 		info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
-		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x + pos.w + 15, pos.y + 60)
-				: Point(pos.x + pos.w -79, pos.y + 195);
-		defenderHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
+		defenderHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, nullptr);
 	}
 	if(owner.attackingHeroInstance)
 	{
 		InfoAboutHero info;
 		info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
-		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x - 93, pos.y + 60)
-				: Point(pos.x + 1, pos.y + 195);
-		attackerHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
+		attackerHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, nullptr);
 	}
 
 	bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool();
@@ -155,6 +158,8 @@ void BattleWindow::createStickyHeroInfoWindows()
 		if(defenderHeroWindow)
 			defenderHeroWindow->disable();
 	}
+
+	setPositionInfoWindow();
 }
 
 void BattleWindow::createTimerInfoWindows()
@@ -222,6 +227,7 @@ void BattleWindow::hideQueue()
 		pos.h -= queue->pos.h;
 		pos = center();
 	}
+	setPositionInfoWindow();
 	GH.windows().totalRedraw();
 }
 
@@ -235,6 +241,7 @@ void BattleWindow::showQueue()
 
 	createQueue();
 	updateQueue();
+	setPositionInfoWindow();
 	GH.windows().totalRedraw();
 }
 
@@ -281,6 +288,38 @@ void BattleWindow::updateQueue()
 	queue->update();
 }
 
+void BattleWindow::setPositionInfoWindow()
+{
+	if(defenderHeroWindow)
+	{
+		Point position = (GH.screenDimensions().x >= 1000)
+				? Point(pos.x + pos.w + 15, pos.y + 60)
+				: Point(pos.x + pos.w -79, pos.y + 195);
+		defenderHeroWindow->moveTo(position);
+	}
+	if(attackerHeroWindow)
+	{
+		Point position = (GH.screenDimensions().x >= 1000)
+				? Point(pos.x - 93, pos.y + 60)
+				: Point(pos.x + 1, pos.y + 195);
+		attackerHeroWindow->moveTo(position);
+	}
+	if(defenderStackWindow)
+	{
+		Point position = (GH.screenDimensions().x >= 1000)
+				? Point(pos.x + pos.w + 15, defenderHeroWindow ? defenderHeroWindow->pos.y + 210 : pos.y + 60)
+				: Point(pos.x + pos.w -79, defenderHeroWindow ? defenderHeroWindow->pos.y : pos.y + 195);
+		defenderStackWindow->moveTo(position);
+	}
+	if(attackerStackWindow)
+	{
+		Point position = (GH.screenDimensions().x >= 1000)
+				? Point(pos.x - 93, attackerHeroWindow ? attackerHeroWindow->pos.y + 210 : pos.y + 60)
+				: Point(pos.x + 1, attackerHeroWindow ? attackerHeroWindow->pos.y : pos.y + 195);
+		attackerStackWindow->moveTo(position);
+	}
+}
+
 void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero)
 {
 	std::shared_ptr<HeroInfoBasicPanel> panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow;
@@ -295,10 +334,7 @@ void BattleWindow::updateStackInfoWindow(const CStack * stack)
 
 	if(stack && stack->unitSide() == BattleSide::DEFENDER)
 	{
-		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x + pos.w + 15, defenderHeroWindow ? defenderHeroWindow->pos.y + 210 : pos.y)
-				: Point(pos.x + pos.w -79, defenderHeroWindow ? defenderHeroWindow->pos.y : pos.y + 135);
-		defenderStackWindow = std::make_shared<StackInfoBasicPanel>(stack, &position);
+		defenderStackWindow = std::make_shared<StackInfoBasicPanel>(stack);
 		defenderStackWindow->setEnabled(showInfoWindows);
 	}
 	else
@@ -306,14 +342,13 @@ void BattleWindow::updateStackInfoWindow(const CStack * stack)
 	
 	if(stack && stack->unitSide() == BattleSide::ATTACKER)
 	{
-		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x - 93, attackerHeroWindow ? attackerHeroWindow->pos.y + 210 : pos.y)
-				: Point(pos.x + 1, attackerHeroWindow ? attackerHeroWindow->pos.y : pos.y + 135);
-		attackerStackWindow = std::make_shared<StackInfoBasicPanel>(stack, &position);
+		attackerStackWindow = std::make_shared<StackInfoBasicPanel>(stack);
 		attackerStackWindow->setEnabled(showInfoWindows);
 	}
 	else
 		attackerStackWindow = nullptr;
+	
+	setPositionInfoWindow();
 }
 
 void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero)
@@ -551,6 +586,12 @@ void BattleWindow::bAutofightf()
 	if (owner.actionsController->spellcastingModeActive())
 		return;
 
+	if(settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman)
+	{
+		endWithAutocombat();
+		return;
+	}
+
 	//Stop auto-fight mode
 	if(owner.curInt->isAutoFightOn)
 	{
@@ -721,7 +762,8 @@ void BattleWindow::blockUI(bool on)
 	setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait);
 	setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode);
-	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, owner.actionsController->spellcastingModeActive());
+	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, (settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) ? on || owner.tacticsMode || owner.actionsController->spellcastingModeActive() : owner.actionsController->spellcastingModeActive());
+	setShortcutBlocked(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, on || owner.tacticsMode || !onlyOnePlayerHuman || owner.actionsController->spellcastingModeActive());
 	setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on && owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on && owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode);
@@ -733,6 +775,39 @@ std::optional<uint32_t> BattleWindow::getQueueHoveredUnitId()
 	return queue->getHoveredUnitIdIfAny();
 }
 
+void BattleWindow::endWithAutocombat() 
+{
+	if(!owner.makingTurn() || owner.tacticsMode)
+		return;
+
+	LOCPLINT->showYesNoDialog(
+		VLC->generaltexth->translate("vcmi.battleWindow.endWithAutocombat"),
+		[this]()
+		{
+			owner.curInt->isAutoFightEndBattle = true;
+
+			auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
+
+			AutocombatPreferences autocombatPreferences = AutocombatPreferences();
+			autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool();
+
+			ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences);
+			ai->battleStart(owner.getBattleID(), owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.getBattle()->battleGetMySide(), false);
+
+			owner.curInt->isAutoFightOn = true;
+			owner.curInt->cb->registerBattleInterface(ai);
+			owner.curInt->autofightingAI = ai;
+
+			owner.requestAutofightingAIToTakeAction();
+
+			close();
+
+			owner.curInt->battleInt.reset();
+		},
+		nullptr
+	);
+}
+
 void BattleWindow::showAll(Canvas & to)
 {
 	CIntObject::showAll(to);

+ 8 - 0
client/battle/BattleWindow.h

@@ -76,6 +76,8 @@ class BattleWindow : public InterfaceObjectConfigurable
 
 	std::shared_ptr<BattleConsole> buildBattleConsole(const JsonNode &) const;
 
+	bool onlyOnePlayerHuman;
+
 public:
 	BattleWindow(BattleInterface & owner );
 	~BattleWindow();
@@ -100,6 +102,9 @@ public:
 	/// Refresh queue after turn order changes
 	void updateQueue();
 
+	// Set positions for hero & stack info window
+	void setPositionInfoWindow();
+
 	/// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side
 	void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero);
 
@@ -125,5 +130,8 @@ public:
 
 	/// Set possible alternative options. If more than 1 - the last will be considered as default option
 	void setAlternativeActions(const std::list<PossiblePlayerBattleAction> &);
+
+	/// ends battle with autocombat
+	void endWithAutocombat();
 };
 

+ 1 - 0
client/gui/Shortcut.h

@@ -125,6 +125,7 @@ enum class EShortcut
 	BATTLE_SURRENDER,
 	BATTLE_RETREAT,
 	BATTLE_AUTOCOMBAT,
+	BATTLE_END_WITH_AUTOCOMBAT,
 	BATTLE_CAST_SPELL,
 	BATTLE_WAIT,
 	BATTLE_DEFEND,

+ 2 - 0
client/gui/ShortcutHandler.cpp

@@ -124,6 +124,7 @@ std::vector<EShortcut> ShortcutHandler::translateKeycode(SDL_Keycode key) const
 		{SDLK_s,         EShortcut::BATTLE_SURRENDER          },
 		{SDLK_r,         EShortcut::BATTLE_RETREAT            },
 		{SDLK_a,         EShortcut::BATTLE_AUTOCOMBAT         },
+		{SDLK_e,         EShortcut::BATTLE_END_WITH_AUTOCOMBAT},
 		{SDLK_c,         EShortcut::BATTLE_CAST_SPELL         },
 		{SDLK_w,         EShortcut::BATTLE_WAIT               },
 		{SDLK_d,         EShortcut::BATTLE_DEFEND             },
@@ -265,6 +266,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"battleSurrender",          EShortcut::BATTLE_SURRENDER          },
 		{"battleRetreat",            EShortcut::BATTLE_RETREAT            },
 		{"battleAutocombat",         EShortcut::BATTLE_AUTOCOMBAT         },
+		{"battleAutocombatEnd",      EShortcut::BATTLE_END_WITH_AUTOCOMBAT},
 		{"battleCastSpell",          EShortcut::BATTLE_CAST_SPELL         },
 		{"battleWait",               EShortcut::BATTLE_WAIT               },
 		{"battleDefend",             EShortcut::BATTLE_DEFEND             },

+ 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"

+ 24 - 12
client/mainmenu/CPrologEpilogVideo.cpp

@@ -14,16 +14,18 @@
 #include "../CGameInfo.h"
 #include "../CMusicHandler.h"
 #include "../CVideoHandler.h"
+#include "../gui/WindowHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/FramerateManager.h"
 #include "../widgets/TextControls.h"
 #include "../render/Canvas.h"
 
 
 CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function<void()> callback)
-	: CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback)
+	: CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback), elapsedTimeMilliseconds(0)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	addUsedEvents(LCLICK);
+	addUsedEvents(LCLICK | TIME);
 	pos = center(Rect(0, 0, 800, 600));
 	updateShadow();
 
@@ -31,15 +33,33 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f
 	videoSoundHandle = CCS->soundh->playSound(audioData);
 	CCS->videoh->open(spe.prologVideo);
 	CCS->musich->playMusic(spe.prologMusic, true, true);
+	voiceDurationMilliseconds = CCS->soundh->getSoundDurationMilliseconds(spe.prologVoice);
 	voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice);
 	auto onVoiceStop = [this]()
 	{
 		voiceStopped = true;
+		elapsedTimeMilliseconds = 0;
 	};
 	CCS->soundh->setCallback(voiceSoundHandle, onVoiceStop);
 
 	text = std::make_shared<CMultiLineLabel>(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText.toString());
-	text->scrollTextTo(-100);
+	text->scrollTextTo(-50); // beginning of text in the vertical middle of black area
+}
+
+void CPrologEpilogVideo::tick(uint32_t msPassed)
+{
+	elapsedTimeMilliseconds += msPassed;
+
+	const uint32_t speed = (voiceDurationMilliseconds == 0) ? 150 : (voiceDurationMilliseconds / (text->textSize.y));
+
+	if(elapsedTimeMilliseconds > speed && text->textSize.y - 50 > positionCounter)
+	{
+		text->scrollTextBy(1);
+		elapsedTimeMilliseconds -= speed;
+		++positionCounter;
+	}
+	else if(elapsedTimeMilliseconds > (voiceDurationMilliseconds == 0 ? 8000 : 3000) && voiceStopped) // pause after completed scrolling (longer for intros missing voice)
+		clickPressed(GH.getCursorPosition());
 }
 
 void CPrologEpilogVideo::show(Canvas & to)
@@ -48,15 +68,7 @@ void CPrologEpilogVideo::show(Canvas & to)
 	//some videos are 800x600 in size while some are 800x400
 	CCS->videoh->update(pos.x, pos.y + (CCS->videoh->size().y == 400 ? 100 : 0), to.getInternalSurface(), true, false);
 
-	//move text every 5 calls/frames; seems to be good enough
-	++positionCounter;
-	if(positionCounter % 5 == 0)
-		text->scrollTextBy(1);
-	else
-		text->showAll(to); // blit text over video, if needed
-
-	if(text->textSize.y + 100 < positionCounter / 5 && voiceStopped)
-		clickPressed(GH.getCursorPosition());
+	text->showAll(to); // blit text over video, if needed
 }
 
 void CPrologEpilogVideo::clickPressed(const Point & cursorPosition)

+ 3 - 0
client/mainmenu/CPrologEpilogVideo.h

@@ -19,6 +19,8 @@ class CPrologEpilogVideo : public CWindowObject
 	CampaignScenarioPrologEpilog spe;
 	int positionCounter;
 	int voiceSoundHandle;
+	uint32_t voiceDurationMilliseconds;
+	uint32_t elapsedTimeMilliseconds;
 	int videoSoundHandle;
 	std::function<void()> exitCb;
 
@@ -29,6 +31,7 @@ class CPrologEpilogVideo : public CWindowObject
 public:
 	CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function<void()> callback);
 
+	void tick(uint32_t msPassed) override;
 	void clickPressed(const Point & cursorPosition) override;
 	void show(Canvas & to) override;
 };

+ 0 - 80
client/widgets/CArtifactsOfHeroAltar.cpp

@@ -20,14 +20,12 @@
 #include "../../lib/networkPacks/ArtifactLocation.h"
 
 CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position)
-	: visibleArtSet(ArtBearer::ArtBearer::HERO)
 {
 	init(
 		std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2),
 		std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2),
 		position,
 		std::bind(&CArtifactsOfHeroAltar::scrollBackpack, this, _1));
-	pickedArtFromSlot = ArtifactPosition::PRE_FIRST;
 
 	// The backpack is in the altar window above and to the right
 	for(auto & slot : backpack)
@@ -40,81 +38,3 @@ CArtifactsOfHeroAltar::~CArtifactsOfHeroAltar()
 {
 	putBackPickedArtifact();
 }
-
-void CArtifactsOfHeroAltar::setHero(const CGHeroInstance * hero)
-{
-	if(hero)
-	{
-		visibleArtSet.artifactsWorn = hero->artifactsWorn;
-		visibleArtSet.artifactsInBackpack = hero->artifactsInBackpack;
-		CArtifactsOfHeroBase::setHero(hero);
-	}
-}
-
-void CArtifactsOfHeroAltar::updateWornSlots()
-{
-	for(auto place : artWorn)
-		setSlotData(getArtPlace(place.first), place.first, visibleArtSet);
-}
-
-void CArtifactsOfHeroAltar::updateBackpackSlots()
-{
-	for(auto artPlace : backpack)
-		setSlotData(getArtPlace(artPlace->slot), artPlace->slot, visibleArtSet);
-}
-
-void CArtifactsOfHeroAltar::scrollBackpack(int offset)
-{
-	CArtifactsOfHeroBase::scrollBackpackForArtSet(offset, visibleArtSet);
-	redraw();
-}
-
-void CArtifactsOfHeroAltar::pickUpArtifact(CArtPlace & artPlace)
-{
-	if(const auto art = artPlace.getArt())
-	{
-		pickedArtFromSlot = artPlace.slot;
-		artPlace.setArtifact(nullptr);
-		deleteFromVisible(art);
-		if(ArtifactUtils::isSlotBackpack(pickedArtFromSlot))
-			pickedArtFromSlot = curHero->getSlotByInstance(art);
-		assert(pickedArtFromSlot != ArtifactPosition::PRE_FIRST);
-		LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, pickedArtFromSlot), ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
-	}
-}
-
-void CArtifactsOfHeroAltar::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc)
-{
-	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
-	const auto pickedArtInst = curHero->getArt(ArtifactPosition::TRANSITION_POS);
-	assert(pickedArtInst);
-	visibleArtSet.putArtifact(dstLoc.slot, const_cast<CArtifactInstance*>(pickedArtInst));
-}
-
-void CArtifactsOfHeroAltar::pickedArtMoveToAltar(const ArtifactPosition & slot)
-{
-	if(ArtifactUtils::isSlotBackpack(slot) || ArtifactUtils::isSlotEquipment(slot) || slot == ArtifactPosition::TRANSITION_POS)
-	{
-		assert(curHero->getSlot(slot)->getArt());
-		LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, slot), ArtifactLocation(curHero->id, pickedArtFromSlot));
-		pickedArtFromSlot = ArtifactPosition::PRE_FIRST;
-	}
-}
-
-void CArtifactsOfHeroAltar::deleteFromVisible(const CArtifactInstance * artInst)
-{
-	const auto slot = visibleArtSet.getSlotByInstance(artInst);
-	visibleArtSet.removeArtifact(slot);
-	if(ArtifactUtils::isSlotBackpack(slot))
-	{
-		scrollBackpackForArtSet(0, visibleArtSet);
-	}
-	else
-	{
-		for(const auto & part : artInst->getPartsInfo())
-		{
-			if(part.slot != ArtifactPosition::PRE_FIRST)
-				getArtPlace(part.slot)->setArtifact(nullptr);
-		}
-	}
-}

+ 0 - 12
client/widgets/CArtifactsOfHeroAltar.h

@@ -16,18 +16,6 @@
 class CArtifactsOfHeroAltar : public CArtifactsOfHeroBase
 {
 public:
-	std::set<const CArtifactInstance*> artifactsOnAltar;
-	ArtifactPosition pickedArtFromSlot;
-	CArtifactFittingSet visibleArtSet;
-
 	CArtifactsOfHeroAltar(const Point & position);
 	~CArtifactsOfHeroAltar();
-	void setHero(const CGHeroInstance * hero) override;
-	void updateWornSlots() override;
-	void updateBackpackSlots() override;
-	void scrollBackpack(int offset) override;
-	void pickUpArtifact(CArtPlace & artPlace);
-	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
-	void pickedArtMoveToAltar(const ArtifactPosition & slot);
-	void deleteFromVisible(const CArtifactInstance * artInst);
 };

+ 4 - 15
client/widgets/CArtifactsOfHeroBackpack.cpp

@@ -41,17 +41,6 @@ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack()
 	initAOHbackpack(visibleCapacityMax, backpackCap < 0 || visibleCapacityMax < backpackCap);
 }
 
-void CArtifactsOfHeroBackpack::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc)
-{
-	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
-}
-
-void CArtifactsOfHeroBackpack::pickUpArtifact(CArtPlace & artPlace)
-{
-	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot),
-		ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
-}
-
 void CArtifactsOfHeroBackpack::scrollBackpack(int offset)
 {
 	if(backpackListBox)
@@ -60,7 +49,7 @@ void CArtifactsOfHeroBackpack::scrollBackpack(int offset)
 	auto slot = ArtifactPosition::BACKPACK_START + backpackPos;
 	for(auto artPlace : backpack)
 	{
-		setSlotData(artPlace, slot, *curHero);
+		setSlotData(artPlace, slot);
 		slot = slot + 1;
 	}
 	redraw();
@@ -188,9 +177,9 @@ void CArtifactsOfHeroQuickBackpack::setHero(const CGHeroInstance * hero)
 		initAOHbackpack(requiredSlots, false);
 		auto artPlace = backpack.begin();
 		for(auto & art : filteredArts)
-			setSlotData(*artPlace++, curHero->getSlotByInstance(art.second), *curHero);
+			setSlotData(*artPlace++, curHero->getSlotByInstance(art.second));
 		for(auto & art : filteredScrolls)
-			setSlotData(*artPlace++, curHero->getSlotByInstance(art.second), *curHero);
+			setSlotData(*artPlace++, curHero->getSlotByInstance(art.second));
 	}
 }
 
@@ -215,5 +204,5 @@ void CArtifactsOfHeroQuickBackpack::swapSelected()
 			break;
 		}
 	if(backpackLoc.slot != ArtifactPosition::PRE_FIRST && filterBySlot != ArtifactPosition::PRE_FIRST && curHero)
-		swapArtifacts(backpackLoc, ArtifactLocation(curHero->id, filterBySlot));
+		LOCPLINT->cb->swapArtifacts(backpackLoc, ArtifactLocation(curHero->id, filterBySlot));
 }

+ 0 - 2
client/widgets/CArtifactsOfHeroBackpack.h

@@ -24,8 +24,6 @@ class CArtifactsOfHeroBackpack : public CArtifactsOfHeroBase
 public:
 	CArtifactsOfHeroBackpack(size_t slotsColumnsMax, size_t slotsRowsMax);
 	CArtifactsOfHeroBackpack();
-	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
-	void pickUpArtifact(CArtPlace & artPlace);
 	void scrollBackpack(int offset) override;
 	void updateBackpackSlots() override;
 	size_t getActiveSlotRowsNum();

+ 9 - 13
client/widgets/CArtifactsOfHeroBase.cpp

@@ -123,7 +123,7 @@ void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero)
 
 	for(auto slot : artWorn)
 	{
-		setSlotData(slot.second, slot.first, *curHero);
+		setSlotData(slot.second, slot.first);
 	}
 	scrollBackpack(0);
 }
@@ -134,16 +134,10 @@ const CGHeroInstance * CArtifactsOfHeroBase::getHero() const
 }
 
 void CArtifactsOfHeroBase::scrollBackpack(int offset)
-{
-	scrollBackpackForArtSet(offset, *curHero);
-	redraw();
-}
-
-void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSet & artSet)
 {
 	// offset==-1 => to left; offset==1 => to right
 	using slotInc = std::function<ArtifactPosition(ArtifactPosition&)>;
-	auto artsInBackpack = static_cast<int>(artSet.artifactsInBackpack.size());
+	auto artsInBackpack = static_cast<int>(curHero->artifactsInBackpack.size());
 	auto scrollingPossible = artsInBackpack > backpack.size();
 
 	slotInc inc_straight = [](ArtifactPosition & slot) -> ArtifactPosition
@@ -170,7 +164,7 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe
 	auto slot = ArtifactPosition(ArtifactPosition::BACKPACK_START + backpackPos);
 	for(auto artPlace : backpack)
 	{
-		setSlotData(artPlace, slot, artSet);
+		setSlotData(artPlace, slot);
 		slot = inc(slot);
 	}
 
@@ -179,6 +173,8 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe
 		leftBackpackRoll->block(!scrollingPossible);
 	if(rightBackpackRoll)
 		rightBackpackRoll->block(!scrollingPossible);
+
+	redraw();
 }
 
 void CArtifactsOfHeroBase::markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved)
@@ -235,7 +231,7 @@ void CArtifactsOfHeroBase::updateBackpackSlots()
 
 void CArtifactsOfHeroBase::updateSlot(const ArtifactPosition & slot)
 {
-	setSlotData(getArtPlace(slot), slot, *curHero);
+	setSlotData(getArtPlace(slot), slot);
 }
 
 const CArtifactInstance * CArtifactsOfHeroBase::getPickedArtifact()
@@ -256,7 +252,7 @@ void CArtifactsOfHeroBase::addGestureCallback(CArtPlace::ClickFunctor callback)
 	}
 }
 
-void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot, const CArtifactSet & artSet)
+void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot)
 {
 	// Spurious call from artifactMoved in attempt to update hidden backpack slot
 	if(!artPlace && ArtifactUtils::isSlotBackpack(slot))
@@ -265,7 +261,7 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit
 	}
 
 	artPlace->slot = slot;
-	if(auto slotInfo = artSet.getSlot(slot))
+	if(auto slotInfo = curHero->getSlot(slot))
 	{
 		artPlace->lockSlot(slotInfo->locked);
 		artPlace->setArtifact(slotInfo->artifact);
@@ -278,7 +274,7 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit
 				arts.insert(std::pair(combinedArt, 0));
 				for(const auto part : combinedArt->getConstituents())
 				{
-					if(artSet.hasArt(part->getId(), false))
+					if(curHero->hasArt(part->getId(), false))
 						arts.at(combinedArt)++;
 				}
 			}

+ 1 - 2
client/widgets/CArtifactsOfHeroBase.h

@@ -69,6 +69,5 @@ protected:
 	virtual void init(CHeroArtPlace::ClickFunctor lClickCallback, CHeroArtPlace::ClickFunctor showPopupCallback,
 		const Point & position, BpackScrollFunctor scrollCallback);
 	// Assigns an artifacts to an artifact place depending on it's new slot ID
-	virtual void setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot, const CArtifactSet & artSet);
-	virtual void scrollBackpackForArtSet(int offset, const CArtifactSet & artSet);
+	virtual void setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot);
 };

+ 0 - 12
client/widgets/CArtifactsOfHeroKingdom.cpp

@@ -50,15 +50,3 @@ CArtifactsOfHeroKingdom::~CArtifactsOfHeroKingdom()
 {
 	putBackPickedArtifact();
 }
-
-void CArtifactsOfHeroKingdom::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc)
-{
-	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
-}
-
-void CArtifactsOfHeroKingdom::pickUpArtifact(CArtPlace & artPlace)
-{
-	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot),
-		ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
-}
-

+ 2 - 3
client/widgets/CArtifactsOfHeroKingdom.h

@@ -20,9 +20,8 @@ VCMI_LIB_NAMESPACE_END
 class CArtifactsOfHeroKingdom : public CArtifactsOfHeroBase
 {
 public:
+	CArtifactsOfHeroKingdom() = delete;
 	CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vector<ArtPlacePtr> Backpack,
 		std::shared_ptr<CButton> leftScroll, std::shared_ptr<CButton> rightScroll);
 	~CArtifactsOfHeroKingdom();
-	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
-	void pickUpArtifact(CArtPlace & artPlace);
-};
+};

+ 0 - 11
client/widgets/CArtifactsOfHeroMain.cpp

@@ -30,14 +30,3 @@ CArtifactsOfHeroMain::~CArtifactsOfHeroMain()
 {
 	putBackPickedArtifact();
 }
-
-void CArtifactsOfHeroMain::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc)
-{
-	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
-}
-
-void CArtifactsOfHeroMain::pickUpArtifact(CArtPlace & artPlace)
-{
-	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot),
-		ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
-}

+ 0 - 2
client/widgets/CArtifactsOfHeroMain.h

@@ -22,6 +22,4 @@ class CArtifactsOfHeroMain : public CArtifactsOfHeroBase
 public:
 	CArtifactsOfHeroMain(const Point & position);
 	~CArtifactsOfHeroMain();
-	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
-	void pickUpArtifact(CArtPlace & artPlace);
 };

+ 2 - 3
client/widgets/CArtifactsOfHeroMarket.cpp

@@ -28,12 +28,12 @@ CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position)
 
 void CArtifactsOfHeroMarket::scrollBackpack(int offset)
 {
-	CArtifactsOfHeroBase::scrollBackpackForArtSet(offset, *curHero);
+	CArtifactsOfHeroBase::scrollBackpack(offset);
 
 	// We may have highlight on one of backpack artifacts
 	if(selectArtCallback)
 	{
-		for(auto & artPlace : backpack)
+		for(const auto & artPlace : backpack)
 		{
 			if(artPlace->isSelected())
 			{
@@ -42,5 +42,4 @@ void CArtifactsOfHeroMarket::scrollBackpack(int offset)
 			}
 		}
 	}
-	redraw();
 }

+ 50 - 41
client/widgets/CWindowWithArtifacts.cpp

@@ -33,6 +33,8 @@
 #include "../../lib/networkPacks/ArtifactLocation.h"
 #include "../../lib/CConfigHandler.h"
 
+#include "../../CCallback.h"
+
 void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet)
 {
 	artSets.emplace_back(artSet);
@@ -81,31 +83,14 @@ const CArtifactInstance * CWindowWithArtifacts::getPickedArtifact()
 
 void CWindowWithArtifacts::clickPressedArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition)
 {
-	const auto artSetWeak = findAOHbyRef(artsInst);
-	assert(artSetWeak.has_value());
+	const auto artSet = findAOHbyRef(artsInst);
+	assert(artSet.has_value());
 
 	if(artPlace.isLocked())
 		return;
 
-	const auto checkSpecialArts = [](const CGHeroInstance * hero, CArtPlace & artPlace) -> bool
-	{
-		if(artPlace.getArt()->getTypeId() == ArtifactID::SPELLBOOK)
-		{
-			GH.windows().createAndPushWindow<CSpellWindow>(hero, LOCPLINT, LOCPLINT->battleInt.get());
-			return false;
-		}
-		if(artPlace.getArt()->getTypeId() == ArtifactID::CATAPULT)
-		{
-			// The Catapult must be equipped
-			std::vector<std::shared_ptr<CComponent>> catapult(1, std::make_shared<CComponent>(ComponentType::ARTIFACT, ArtifactID(ArtifactID::CATAPULT)));
-			LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[312], catapult);
-			return false;
-		}
-		return true;
-	};
-
 	std::visit(
-		[checkSpecialArts, this, &artPlace](auto artSetWeak) -> void
+		[this, &artPlace](auto artSetWeak) -> void
 		{
 			const auto artSetPtr = artSetWeak.lock();
 
@@ -153,26 +138,23 @@ void CWindowWithArtifacts::clickPressedArtPlaceHero(CArtifactsOfHeroBase & artsI
 							isTransferAllowed = false;
 					}
 					if(isTransferAllowed)
-						artSetPtr->swapArtifacts(srcLoc, dstLoc);
+						LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
 				}
-				else
+				else if(auto art = artPlace.getArt())
 				{
-					if(artPlace.getArt())
+					if(artSetPtr->getHero()->getOwner() == LOCPLINT->playerID)
 					{
-						if(artSetPtr->getHero()->tempOwner == LOCPLINT->playerID)
-						{
-							if(checkSpecialArts(hero, artPlace))
-								artSetPtr->pickUpArtifact(artPlace);
-						}
-						else
-						{
-							for(const auto artSlot : ArtifactUtils::unmovableSlots())
-								if(artPlace.slot == artSlot)
-								{
-									msg = CGI->generaltexth->allTexts[21];
-									break;
-								}
-						}
+						if(checkSpecialArts(*art, hero, std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroAltar>> ? true : false))
+							LOCPLINT->cb->swapArtifacts(ArtifactLocation(artSetPtr->getHero()->id, artPlace.slot), ArtifactLocation(artSetPtr->getHero()->id, ArtifactPosition::TRANSITION_POS));
+					}
+					else
+					{
+						for(const auto artSlot : ArtifactUtils::unmovableSlots())
+							if(artPlace.slot == artSlot)
+							{
+								msg = CGI->generaltexth->allTexts[21];
+								break;
+							}
 					}
 				}
 
@@ -211,12 +193,11 @@ void CWindowWithArtifacts::clickPressedArtPlaceHero(CArtifactsOfHeroBase & artsI
 			else if constexpr(std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroQuickBackpack>>)
 			{
 				const auto hero = artSetPtr->getHero();
-				artSetPtr->swapArtifacts(ArtifactLocation(hero->id, artPlace.slot),
-					ArtifactLocation(hero->id, artSetPtr->getFilterSlot()));
+				LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, artPlace.slot), ArtifactLocation(hero->id, artSetPtr->getFilterSlot()));
 				if(closeCallback)
 					closeCallback();
 			}
-		}, artSetWeak.value());
+		}, artSet.value());
 }
 
 void CWindowWithArtifacts::showPopupArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition)
@@ -234,6 +215,7 @@ void CWindowWithArtifacts::showPopupArtPlaceHero(CArtifactsOfHeroBase & artsInst
 
 			// Hero (Main, Exchange) window, Kingdom window, Backpack window right click handler
 			if constexpr(
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroAltar>> ||
 				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroMain>> ||
 				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroKingdom>> ||
 				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroBackpack>>)
@@ -254,7 +236,6 @@ void CWindowWithArtifacts::showPopupArtPlaceHero(CArtifactsOfHeroBase & artsInst
 			}
 			// Altar window, Market window right click handler
 			else if constexpr(
-				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroAltar>> ||
 				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroMarket>> ||
 				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroQuickBackpack>>)
 			{
@@ -469,3 +450,31 @@ void CWindowWithArtifacts::markPossibleSlots()
 			std::visit(artifactAssembledBody, artSetWeak);
 	}
 }
+
+bool CWindowWithArtifacts::checkSpecialArts(const CArtifactInstance & artInst, const CGHeroInstance * hero, bool isTrade)
+{
+	const auto artId = artInst.getTypeId();
+	
+	if(artId == ArtifactID::SPELLBOOK)
+	{
+		GH.windows().createAndPushWindow<CSpellWindow>(hero, LOCPLINT, LOCPLINT->battleInt.get());
+		return false;
+	}
+	if(artId == ArtifactID::CATAPULT)
+	{
+		// The Catapult must be equipped
+		LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[312],
+			std::vector<std::shared_ptr<CComponent>>(1, std::make_shared<CComponent>(ComponentType::ARTIFACT, ArtifactID(ArtifactID::CATAPULT))));
+		return false;
+	}
+	if(isTrade)
+	{
+		if(!artInst.artType->isTradable())
+		{
+			LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[21],
+				std::vector<std::shared_ptr<CComponent>>(1, std::make_shared<CComponent>(ComponentType::ARTIFACT, artId)));
+			return false;
+		}
+	}
+	return true;
+}

+ 1 - 0
client/widgets/CWindowWithArtifacts.h

@@ -50,4 +50,5 @@ protected:
 	std::optional<std::tuple<const CGHeroInstance*, const CArtifactInstance*>> getState();
 	std::optional<CArtifactsOfHeroPtr> findAOHbyRef(CArtifactsOfHeroBase & artsInst);
 	void markPossibleSlots();
+	bool checkSpecialArts(const CArtifactInstance & artInst, const CGHeroInstance * hero, bool isTrade);
 };

+ 101 - 99
client/widgets/markets/CAltarArtifacts.cpp

@@ -12,7 +12,6 @@
 #include "CAltarArtifacts.h"
 
 #include "../../gui/CGuiHandler.h"
-#include "../../gui/CursorHandler.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -31,6 +30,11 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance *
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
 
+	assert(market);
+	auto altarObj = dynamic_cast<const CGArtifactsAltar*>(market);
+	altarId = altarObj->id;
+	altarArtifacts = altarObj;
+
 	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("ALTSACR.DEF"),
 		CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); });
 	labels.emplace_back(std::make_shared<CLabel>(450, 34, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477]));
@@ -46,8 +50,8 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance *
 		CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this));
 	sacrificeBackpackButton->block(hero->artifactsInBackpack.empty());
 
-	arts = std::make_shared<CArtifactsOfHeroAltar>(Point(-365, -11));
-	arts->setHero(hero);
+	heroArts = std::make_shared<CArtifactsOfHeroAltar>(Point(-365, -11));
+	heroArts->setHero(hero);
 
 	int slotNum = 0;
 	for(auto & altarSlotPos : posSlotsAltar)
@@ -65,151 +69,149 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance *
 
 TExpType CAltarArtifacts::calcExpAltarForHero()
 {
-	auto artifactsOfHero = std::dynamic_pointer_cast<CArtifactsOfHeroAltar>(arts);
 	TExpType expOnAltar(0);
-	for(const auto art : artifactsOfHero->artifactsOnAltar)
-	{
-		int dmp = 0;
-		int expOfArt = 0;
-		market->getOffer(art->getTypeId(), 0, dmp, expOfArt, EMarketMode::ARTIFACT_EXP);
-		expOnAltar += expOfArt;
-	}
-	auto resultExp = hero->calculateXp(expOnAltar);
-	expForHero->setText(std::to_string(resultExp));
-	return resultExp;
+	for(const auto & tradeSlot : tradeSlotsMap)
+		expOnAltar += calcExpCost(tradeSlot.first);
+	expForHero->setText(std::to_string(expOnAltar));
+	return expOnAltar;
 }
 
 void CAltarArtifacts::makeDeal()
 {
 	std::vector<TradeItemSell> positions;
-	for(const auto art : arts->artifactsOnAltar)
+	for(const auto & [artInst, altarSlot] : tradeSlotsMap)
 	{
-		positions.push_back(hero->getSlotByInstance(art));
+		positions.push_back(artInst->getId());
 	}
-	std::sort(positions.begin(), positions.end());
-	std::reverse(positions.begin(), positions.end());
-
 	LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_EXP, positions, std::vector<TradeItemBuy>(), std::vector<ui32>(), hero);
-	arts->artifactsOnAltar.clear();
 
+	tradeSlotsMap.clear();
+	// The event for removing artifacts from the altar will not be triggered. Therefore, we clean the altar immediately.
 	for(auto item : items[0])
 	{
 		item->setID(-1);
 		item->subtitle.clear();
 	}
-	deal->block(true);
 	calcExpAltarForHero();
+	deal->block(tradeSlotsMap.empty());
 }
 
 void CAltarArtifacts::sacrificeAll()
 {
-	std::vector<ConstTransitivePtr<CArtifactInstance>> artsForMove;
-	for(const auto & [slot, slotInfo] : arts->getHero()->artifactsWorn)
-	{
-		if(!slotInfo.locked && slotInfo.artifact->artType->isTradable())
-			artsForMove.emplace_back(slotInfo.artifact);
-	}
-	for(auto artInst : artsForMove)
-		moveArtToAltar(nullptr, artInst);
-	arts->updateWornSlots();
-	sacrificeBackpack();
+	LOCPLINT->cb->bulkMoveArtifacts(heroArts->getHero()->id, altarId, false, true, true);
 }
 
 void CAltarArtifacts::sacrificeBackpack()
 {
-	while(!arts->visibleArtSet.artifactsInBackpack.empty())
-	{
-		if(!putArtOnAltar(nullptr, arts->visibleArtSet.artifactsInBackpack[0].artifact))
-			break;
-	};
-	calcExpAltarForHero();
+	LOCPLINT->cb->bulkMoveArtifacts(heroArts->getHero()->id, altarId, false, false, true);
 }
 
 void CAltarArtifacts::setSelectedArtifact(const CArtifactInstance * art)
 {
-	if(art)
-	{
-		selectedArt->setArtifact(art);
-		int dmp = 0;
-		int exp = 0;
-		market->getOffer(art->getTypeId(), 0, dmp, exp, EMarketMode::ARTIFACT_EXP);
-		selectedCost->setText(std::to_string(hero->calculateXp(exp)));
-	}
-	else
-	{
-		selectedArt->setArtifact(nullptr);
-		selectedCost->setText("");
-	}
+	selectedArt->setArtifact(art);
+	selectedCost->setText(art == nullptr ? "" : std::to_string(calcExpCost(art)));
 }
 
-void CAltarArtifacts::moveArtToAltar(std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance * art)
+std::shared_ptr<CArtifactsOfHeroAltar> CAltarArtifacts::getAOHset() const
 {
-	if(putArtOnAltar(altarSlot, art))
-	{
-		CCS->curh->dragAndDropCursor(nullptr);
-		arts->unmarkSlots();
-	}
+	return heroArts;
 }
 
-std::shared_ptr<CArtifactsOfHeroAltar> CAltarArtifacts::getAOHset() const
+ObjectInstanceID CAltarArtifacts::getObjId() const
 {
-	return arts;
+	return altarId;
 }
 
-bool CAltarArtifacts::putArtOnAltar(std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance * art)
+void CAltarArtifacts::updateSlots()
 {
-	if(!art->artType->isTradable())
+	assert(altarArtifacts->artifactsInBackpack.size() <= GameConstants::ALTAR_ARTIFACTS_SLOTS);
+	assert(tradeSlotsMap.size() <= GameConstants::ALTAR_ARTIFACTS_SLOTS);
+	
+	auto slotsToAdd = tradeSlotsMap;
+	for(auto & altarSlot : items[0])
+		if(altarSlot->id != -1)
+		{
+			if(tradeSlotsMap.find(altarSlot->getArtInstance()) == tradeSlotsMap.end())
+			{
+				altarSlot->setID(-1);
+				altarSlot->subtitle.clear();
+			}
+			else
+			{
+				slotsToAdd.erase(altarSlot->getArtInstance());
+			}
+		}
+
+	for(auto & tradeSlot : slotsToAdd)
 	{
-		logGlobal->warn("Cannot put special artifact on altar!");
-		return false;
+		assert(tradeSlot.second->id == -1);
+		assert(altarArtifacts->getSlotByInstance(tradeSlot.first) != ArtifactPosition::PRE_FIRST);
+		tradeSlot.second->setArtInstance(tradeSlot.first);
+		tradeSlot.second->subtitle = std::to_string(calcExpCost(tradeSlot.first));
 	}
-
-	if(!altarSlot || altarSlot->id != -1)
+	for(auto & slotInfo : altarArtifacts->artifactsInBackpack)
 	{
-		int slotIndex = -1;
-		while(items[0][++slotIndex]->id >= 0 && slotIndex + 1 < items[0].size());
-		slotIndex = items[0][slotIndex]->id == -1 ? slotIndex : -1;
-		if(slotIndex < 0)
+		if(tradeSlotsMap.find(slotInfo.artifact) == tradeSlotsMap.end())
 		{
-			logGlobal->warn("No free slots on altar!");
-			return false;
+			for(auto & altarSlot : items[0])
+				if(altarSlot->id == -1)
+				{
+					altarSlot->setArtInstance(slotInfo.artifact);
+					altarSlot->subtitle = std::to_string(calcExpCost(slotInfo.artifact));
+					tradeSlotsMap.try_emplace(slotInfo.artifact, altarSlot);
+					break;
+				}
 		}
-		altarSlot = items[0][slotIndex];
 	}
+	calcExpAltarForHero();
+	deal->block(tradeSlotsMap.empty());
+}
 
-	int dmp = 0;
-	int exp = 0;
-	market->getOffer(art->artType->getId(), 0, dmp, exp, EMarketMode::ARTIFACT_EXP);
-	exp = static_cast<int>(hero->calculateXp(exp));
-
-	arts->artifactsOnAltar.insert(art);
-	altarSlot->setArtInstance(art);
-	altarSlot->subtitle = std::to_string(exp);
-
-	deal->block(false);
-	return true;
-};
+void CAltarArtifacts::putBackArtifacts()
+{
+	// TODO: If the backpack capacity limit is enabled, artifacts may remain on the altar.
+	// Perhaps should be erased in CGameHandler::objectVisitEnded if id of visited object will be available
+	if(!altarArtifacts->artifactsInBackpack.empty())
+		LOCPLINT->cb->bulkMoveArtifacts(altarId, heroArts->getHero()->id, false, true, true);
+}
 
-void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSlot)
+void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & altarSlot, std::shared_ptr<CTradeableItem> & hCurSlot)
 {
-	const auto pickedArtInst = arts->getPickedArtifact();
-	if(pickedArtInst)
+	assert(altarSlot);
+
+	if(const auto pickedArtInst = heroArts->getPickedArtifact())
 	{
-		arts->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS);
-		moveArtToAltar(newSlot, pickedArtInst);
+		if(pickedArtInst->canBePutAt(altarArtifacts))
+		{
+			if(pickedArtInst->artType->isTradable())
+			{
+				if(altarSlot->id == -1)
+					tradeSlotsMap.try_emplace(pickedArtInst, altarSlot);
+				deal->block(false);
+
+				LOCPLINT->cb->swapArtifacts(ArtifactLocation(heroArts->getHero()->id, ArtifactPosition::TRANSITION_POS),
+					ArtifactLocation(altarId, ArtifactPosition::ALTAR));
+			}
+			else
+			{
+				logGlobal->warn("Cannot put special artifact on altar!");
+				return;
+			}
+		}
 	}
-	else if(const CArtifactInstance * art = newSlot->getArtInstance())
+	else if(const CArtifactInstance * art = altarSlot->getArtInstance())
 	{
-		const auto hero = arts->getHero();
-		const auto slot = hero->getSlotByInstance(art);
+		const auto slot = altarArtifacts->getSlotByInstance(art);
 		assert(slot != ArtifactPosition::PRE_FIRST);
-		LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, slot),
-			ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS));
-		arts->pickedArtFromSlot = slot;
-		arts->artifactsOnAltar.erase(art);
-		newSlot->setID(-1);
-		newSlot->subtitle.clear();
-		deal->block(!arts->artifactsOnAltar.size());
+		LOCPLINT->cb->swapArtifacts(ArtifactLocation(altarId, slot), ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS));
+		tradeSlotsMap.erase(art);
 	}
-	calcExpAltarForHero();
+}
+
+TExpType CAltarArtifacts::calcExpCost(const CArtifactInstance * art)
+{
+	int dmp = 0;
+	int expOfArt = 0;
+	market->getOffer(art->getTypeId(), 0, dmp, expOfArt, EMarketMode::ARTIFACT_EXP);
+	return hero->calculateXp(expOfArt);
 }

+ 9 - 4
client/widgets/markets/CAltarArtifacts.h

@@ -21,14 +21,19 @@ public:
 	void sacrificeAll() override;
 	void sacrificeBackpack();
 	void setSelectedArtifact(const CArtifactInstance * art);
-	void moveArtToAltar(std::shared_ptr<CTradeableItem>, const CArtifactInstance * art);
 	std::shared_ptr<CArtifactsOfHeroAltar> getAOHset() const;
+	ObjectInstanceID getObjId() const;
+	void updateSlots();
+	void putBackArtifacts();
 
 private:
+	ObjectInstanceID altarId;
+	const CArtifactSet * altarArtifacts;
 	std::shared_ptr<CArtPlace> selectedArt;
 	std::shared_ptr<CLabel> selectedCost;
 	std::shared_ptr<CButton> sacrificeBackpackButton;
-	std::shared_ptr<CArtifactsOfHeroAltar> arts;
+	std::shared_ptr<CArtifactsOfHeroAltar> heroArts;
+	std::map<const CArtifactInstance*, std::shared_ptr<CTradeableItem>> tradeSlotsMap;
 
 	const std::vector<Point> posSlotsAltar =
 	{
@@ -42,6 +47,6 @@ private:
 		Point(452, 333)
 	};
 
-	bool putArtOnAltar(std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance * art);
-	void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSlot) override;
+	void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & altarSlot, std::shared_ptr<CTradeableItem> & hCurSlot) override;
+	TExpType calcExpCost(const CArtifactInstance * art);
 };

+ 10 - 2
client/windows/CAltarWindow.cpp

@@ -19,6 +19,7 @@
 
 #include "../CGameInfo.h"
 
+#include "../lib/networkPacks/ArtifactLocation.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
@@ -78,14 +79,18 @@ void CAltarWindow::createAltarArtifacts(const IMarket * market, const CGHeroInst
 	auto altarArtifacts = std::make_shared<CAltarArtifacts>(market, hero);
 	altar = altarArtifacts;
 	artSets.clear();
-	addSetAndCallbacks(altarArtifacts->getAOHset());
+	addSetAndCallbacks(altarArtifacts->getAOHset()); altarArtifacts->putBackArtifacts();
 
 	changeModeButton = std::make_shared<CButton>(Point(516, 421), AnimationPath::builtin("ALTSACC.DEF"),
 		CGI->generaltexth->zelp[572], std::bind(&CAltarWindow::createAltarCreatures, this, market, hero));
 	if(altar->hero->getAlignment() == EAlignment::GOOD)
 		changeModeButton->block(true);
 	quitButton = std::make_shared<CButton>(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"),
-		CGI->generaltexth->zelp[568], std::bind(&CAltarWindow::close, this), EShortcut::GLOBAL_RETURN);
+		CGI->generaltexth->zelp[568], [this, altarArtifacts]()
+		{
+			altarArtifacts->putBackArtifacts();
+			CAltarWindow::close();
+		}, EShortcut::GLOBAL_RETURN);
 	altar->setRedrawParent(true);
 	redraw();
 }
@@ -115,6 +120,9 @@ void CAltarWindow::artifactMoved(const ArtifactLocation & srcLoc, const Artifact
 
 	if(auto altarArtifacts = std::static_pointer_cast<CAltarArtifacts>(altar))
 	{
+		if(srcLoc.artHolder == altarArtifacts->getObjId() || destLoc.artHolder == altarArtifacts->getObjId())
+			altarArtifacts->updateSlots();
+
 		if(const auto pickedArt = getPickedArtifact())
 			altarArtifacts->setSelectedArtifact(pickedArt);
 		else

+ 18 - 8
client/windows/CCastleInterface.cpp

@@ -34,6 +34,7 @@
 #include "../render/Canvas.h"
 #include "../render/IImage.h"
 #include "../render/IRenderHandler.h"
+#include "../render/CAnimation.h"
 #include "../render/ColorFilter.h"
 #include "../adventureMap/AdventureMapInterface.h"
 #include "../adventureMap/CList.h"
@@ -872,7 +873,7 @@ void CCastleBuildings::enterCastleGate()
 		return;//only visiting hero can use castle gates
 	}
 	std::vector <int> availableTowns;
-	std::vector <const CGTownInstance*> Towns = LOCPLINT->cb->getTownsInfo(true);
+	std::vector <const CGTownInstance*> Towns = LOCPLINT->localState->getOwnedTowns();
 	for(auto & Town : Towns)
 	{
 		const CGTownInstance *t = Town;
@@ -883,9 +884,22 @@ void CCastleBuildings::enterCastleGate()
 			availableTowns.push_back(t->id.getNum());//add to the list
 		}
 	}
+
+	std::vector<std::shared_ptr<IImage>> images;
+	for(auto & t : Towns)
+	{
+		if(!settings["general"]["enableUiEnhancements"].Bool())
+			break;
+		std::shared_ptr<CAnimation> a = GH.renderHandler().loadAnimation(AnimationPath::builtin("ITPA"));
+		a->preload();
+		images.push_back(a->getImage(t->town->clientInfo.icons[t->hasFort()][false] + 2)->scaleFast(Point(35, 23)));
+	}
+
 	auto gateIcon = std::make_shared<CAnimImage>(town->town->clientInfo.buildingsIcons, BuildingID::CASTLE_GATE);//will be deleted by selection window
-	GH.windows().createAndPushWindow<CObjectListWindow>(availableTowns, gateIcon, CGI->generaltexth->jktexts[40],
-		CGI->generaltexth->jktexts[41], std::bind (&CCastleInterface::castleTeleport, LOCPLINT->castleInt, _1));
+	auto wnd = std::make_shared<CObjectListWindow>(availableTowns, gateIcon, CGI->generaltexth->jktexts[40],
+		CGI->generaltexth->jktexts[41], std::bind (&CCastleInterface::castleTeleport, LOCPLINT->castleInt, _1), 0, images);
+	wnd->onPopup = [Towns](int index) { CRClickPopup::createAndPush(Towns[index], GH.getCursorPosition()); };
+	GH.windows().pushWindow(wnd);
 }
 
 void CCastleBuildings::enterDwelling(int level)
@@ -967,11 +981,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());
 

+ 2 - 2
client/windows/CSpellWindow.cpp

@@ -519,13 +519,13 @@ void CSpellWindow::setCurrentPage(int value)
 void CSpellWindow::turnPageLeft()
 {
 	if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook)
-		CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15);
+		CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15, EVideoType::SPELLBOOK);
 }
 
 void CSpellWindow::turnPageRight()
 {
 	if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook)
-		CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15);
+		CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15, EVideoType::SPELLBOOK);
 }
 
 void CSpellWindow::keyPressed(EShortcut key)

+ 24 - 10
client/windows/GUIClasses.cpp

@@ -39,6 +39,7 @@
 #include "../render/Canvas.h"
 #include "../render/CAnimation.h"
 #include "../render/IRenderHandler.h"
+#include "../render/IImage.h"
 
 #include "../../CCallback.h"
 
@@ -424,7 +425,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 +586,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 +640,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 +707,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);
 	};
 
@@ -1674,11 +1675,13 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner):
 }
 
 CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::string _text)
-	: CIntObject(LCLICK | DOUBLECLICK),
+	: CIntObject(LCLICK | DOUBLECLICK | RCLICK_POPUP),
 	parent(_parent),
 	index(_id)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	if(parent->images.size() > index)
+		icon = std::make_shared<CPicture>(parent->images[index], Point(1, 1));
 	border = std::make_shared<CPicture>(ImagePath::builtin("TPGATES"));
 	pos = border->pos;
 
@@ -1701,6 +1704,9 @@ void CObjectListWindow::CItem::select(bool on)
 void CObjectListWindow::CItem::clickPressed(const Point & cursorPosition)
 {
 	parent->changeSelection(index);
+
+	if(parent->onClicked)
+		parent->onClicked(index);
 }
 
 void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition)
@@ -1708,10 +1714,17 @@ void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition)
 	parent->elementSelected();
 }
 
-CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection)
+void CObjectListWindow::CItem::showPopupWindow(const Point & cursorPosition)
+{
+	if(parent->onPopup)
+		parent->onPopup(index);
+}
+
+CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection, std::vector<std::shared_ptr<IImage>> images)
 	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")),
 	onSelect(Callback),
-	selected(initialSelection)
+	selected(initialSelection),
+	images(images)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	items.reserve(_items.size());
@@ -1724,10 +1737,11 @@ CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::share
 	init(titleWidget_, _title, _descr);
 }
 
-CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection)
+CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection, std::vector<std::shared_ptr<IImage>> images)
 	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")),
 	onSelect(Callback),
-	selected(initialSelection)
+	selected(initialSelection),
+	images(images)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	items.reserve(_items.size());
@@ -1805,7 +1819,7 @@ void CObjectListWindow::changeSelection(size_t which)
 	selected = which;
 }
 
-void CObjectListWindow::keyPressed (EShortcut key)
+void CObjectListWindow::keyPressed(EShortcut key)
 {
 	int sel = static_cast<int>(selected);
 

+ 8 - 2
client/windows/GUIClasses.h

@@ -38,6 +38,7 @@ class CGarrisonSlot;
 class CHeroArea;
 class CAnimImage;
 class CFilledTexture;
+class IImage;
 
 enum class EUserEvent;
 
@@ -157,6 +158,7 @@ class CObjectListWindow : public CWindowObject
 		CObjectListWindow * parent;
 		std::shared_ptr<CLabel> text;
 		std::shared_ptr<CPicture> border;
+		std::shared_ptr<CPicture> icon;
 	public:
 		const size_t index;
 		CItem(CObjectListWindow * parent, size_t id, std::string text);
@@ -164,12 +166,14 @@ class CObjectListWindow : public CWindowObject
 		void select(bool on);
 		void clickPressed(const Point & cursorPosition) override;
 		void clickDouble(const Point & cursorPosition) override;
+		void showPopupWindow(const Point & cursorPosition) override;
 	};
 
 	std::function<void(int)> onSelect;//called when OK button is pressed, returns id of selected item.
 	std::shared_ptr<CIntObject> titleWidget;
 	std::shared_ptr<CLabel> title;
 	std::shared_ptr<CLabel> descr;
+	std::vector<std::shared_ptr<IImage>> images;
 
 	std::shared_ptr<CListBox> list;
 	std::shared_ptr<CButton> ok;
@@ -183,12 +187,14 @@ public:
 	size_t selected;//index of currently selected item
 
 	std::function<void()> onExit;//optional exit callback
+	std::function<void(int)> onPopup;//optional popup callback
+	std::function<void(int)> onClicked;//optional if clicked on item callback
 
 	/// Callback will be called when OK button is pressed, returns id of selected item. initState = initially selected item
 	/// Image can be nullptr
 	///item names will be taken from map objects
-	CObjectListWindow(const std::vector<int> &_items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection = 0);
-	CObjectListWindow(const std::vector<std::string> &_items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection = 0);
+	CObjectListWindow(const std::vector<int> &_items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection = 0, std::vector<std::shared_ptr<IImage>> images = {});
+	CObjectListWindow(const std::vector<std::string> &_items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection = 0, std::vector<std::shared_ptr<IImage>> images = {});
 
 	std::shared_ptr<CIntObject> genItem(size_t index);
 	void elementSelected();//call callback and close this window

+ 12 - 0
client/windows/settings/BattleOptionsTab.cpp

@@ -68,6 +68,10 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner)
 	{
 		enableAutocombatSpellsChangedCallback(value);
 	});
+	addCallback("endWithAutocombatChanged", [this](bool value)
+	{
+		endWithAutocombatChangedCallback(value);
+	});
 	build(config);
 
 	std::shared_ptr<CToggleGroup> animationSpeedToggle = widget<CToggleGroup>("animationSpeedPicker");
@@ -99,6 +103,9 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner)
 
 	std::shared_ptr<CToggleButton> enableAutocombatSpellsCheckbox = widget<CToggleButton>("enableAutocombatSpellsCheckbox");
 	enableAutocombatSpellsCheckbox->setSelected(settings["battle"]["enableAutocombatSpells"].Bool());
+
+	std::shared_ptr<CToggleButton> endWithAutocombatCheckbox = widget<CToggleButton>("endWithAutocombatCheckbox");
+	endWithAutocombatCheckbox->setSelected(settings["battle"]["endWithAutocombat"].Bool());
 }
 
 int BattleOptionsTab::getAnimSpeed() const
@@ -248,3 +255,8 @@ void BattleOptionsTab::enableAutocombatSpellsChangedCallback(bool value)
 	enableAutocombatSpells->Bool() = value;
 }
 
+void BattleOptionsTab::endWithAutocombatChangedCallback(bool value)
+{
+	Settings endWithAutocombat = settings.write["battle"]["endWithAutocombat"];
+	endWithAutocombat->Bool() = value;
+}

+ 1 - 0
client/windows/settings/BattleOptionsTab.h

@@ -33,6 +33,7 @@ private:
 	void skipBattleIntroMusicChangedCallback(bool value);
 	void showStickyHeroWindowsChangedCallback(bool value, BattleInterface * parentBattleInterface);
 	void enableAutocombatSpellsChangedCallback(bool value);
+	void endWithAutocombatChangedCallback(bool value);
 public:
 	BattleOptionsTab(BattleInterface * owner = nullptr);
 };

+ 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",

+ 5 - 1
config/schemas/settings.json

@@ -304,7 +304,7 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default" : {},
-			"required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangeLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize", "stickyHeroInfoWindows", "enableAutocombatSpells" ],
+			"required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangeLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize", "stickyHeroInfoWindows", "enableAutocombatSpells", "endWithAutocombat" ],
 			"properties" : {
 				"speedFactor" : {
 					"type" : "number",
@@ -350,6 +350,10 @@
 				"enableAutocombatSpells" : {
 					"type": "boolean",
 					"default": true
+				},
+				"endWithAutocombat" : {
+					"type": "boolean",
+					"default": false
 				}
 			}
 		},

+ 17 - 0
config/widgets/settings/battleOptionsTab.json

@@ -47,6 +47,9 @@
 				},
 				{
 					"text": "core.genrltxt.401" // First Aid Tent
+				},
+				{
+					"text": "vcmi.battleOptions.endWithAutocombat.hover"
 				}
 			]
 		},
@@ -86,6 +89,20 @@
 				{}
 			]
 		},
+
+		{
+			"type" : "verticalLayout",
+			"customType" : "checkbox",
+			"position": {"x": 380, "y": 233},
+			"items":
+			[
+				{
+					"help": "vcmi.battleOptions.endWithAutocombat",
+					"name": "endWithAutocombatCheckbox",
+					"callback": "endWithAutocombatChanged"
+				}
+			]
+		},
 /////////////////////////////////////// Left section - checkboxes
 		{
 			"name": "creatureInfoLabels",

+ 2 - 2
docs/Readme.md

@@ -1,8 +1,8 @@
-[![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?event=schedule)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=event%3Aschedule)
+[![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.0)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.1)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.2)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.4/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.4)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.5/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.5)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 
 # VCMI Project

+ 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)
 	{

+ 9 - 4
lib/ArtifactUtils.cpp

@@ -151,11 +151,16 @@ DLL_LINKAGE bool ArtifactUtils::isSlotEquipment(const ArtifactPosition & slot)
 
 DLL_LINKAGE bool ArtifactUtils::isBackpackFreeSlots(const CArtifactSet * target, const size_t reqSlots)
 {
-	const auto backpackCap = VLC->settings()->getInteger(EGameSettings::HEROES_BACKPACK_CAP);
-	if(backpackCap < 0)
-		return true;
+	if(target->bearerType() == ArtBearer::HERO)
+	{
+		const auto backpackCap = VLC->settings()->getInteger(EGameSettings::HEROES_BACKPACK_CAP);
+		if(backpackCap < 0)
+			return true;
+		else
+			return target->artifactsInBackpack.size() + reqSlots <= backpackCap;
+	}
 	else
-		return target->artifactsInBackpack.size() + reqSlots <= backpackCap;
+		return false;
 }
 
 DLL_LINKAGE std::vector<const CArtifact*> ArtifactUtils::assemblyPossibilities(

+ 8 - 1
lib/CArtHandler.cpp

@@ -181,7 +181,7 @@ bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, b
 {
 	auto simpleArtCanBePutAt = [this](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool
 	{
-		if(ArtifactUtils::isSlotBackpack(slot))
+		if(artSet->bearerType() == ArtBearer::HERO && ArtifactUtils::isSlotBackpack(slot))
 		{
 			if(isBig() || (!assumeDestRemoved && !ArtifactUtils::isBackpackFreeSlots(artSet)))
 				return false;
@@ -258,6 +258,7 @@ CArtifact::CArtifact()
 	possibleSlots[ArtBearer::HERO]; //we want to generate map entry even if it will be empty
 	possibleSlots[ArtBearer::CREATURE]; //we want to generate map entry even if it will be empty
 	possibleSlots[ArtBearer::COMMANDER];
+	possibleSlots[ArtBearer::ALTAR];
 }
 
 //This destructor should be placed here to avoid side effects
@@ -476,6 +477,9 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode
 		}
 	});
 
+	if(art->isTradable())
+		art->possibleSlots.at(ArtBearer::ALTAR).push_back(ArtifactPosition::ALTAR);
+
 	return art;
 }
 
@@ -906,6 +910,9 @@ const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const
 
 bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck) const
 {
+	if(bearerType() == ArtBearer::ALTAR)
+		return artifactsInBackpack.size() < GameConstants::ALTAR_ARTIFACTS_SLOTS;
+
 	if(const ArtSlotInfo *s = getSlot(pos))
 		return (onlyLockCheck || !s->artifact) && !s->locked;
 

+ 2 - 1
lib/CArtHandler.h

@@ -30,7 +30,8 @@ class JsonSerializeFormat;
 #define ART_BEARER_LIST \
 	ART_BEARER(HERO)\
 	ART_BEARER(CREATURE)\
-	ART_BEARER(COMMANDER)
+	ART_BEARER(COMMANDER)\
+	ART_BEARER(ALTAR)
 
 namespace ArtBearer
 {

+ 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
 {

+ 2 - 27
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()]);
-	}
-}
-
 /************************************************************************/
 /*                                                                      */
 /************************************************************************/
@@ -968,20 +954,9 @@ const CGObjectInstance * CGameInfoCallback::getObjInstance( ObjectInstanceID oid
 	return gs->map->objects[oid.num];
 }
 
-CArtifactSet * CGameInfoCallback::getArtSet(const ArtifactLocation & loc) const
+const CArtifactSet * CGameInfoCallback::getArtSet(const ArtifactLocation & loc) const
 {
-	auto hero = const_cast<CGHeroInstance*>(getHero(loc.artHolder));
-	if(loc.creature.has_value())
-	{
-		if(loc.creature.value() == SlotID::COMMANDER_SLOT_PLACEHOLDER)
-			return hero->commander;
-		else
-			return hero->getStackPtr(loc.creature.value());
-	}
-	else
-	{
-		return hero;
-	}
+	return gs->getArtSet(loc);
 }
 
 std::vector<ObjectInstanceID> CGameInfoCallback::getVisibleTeleportObjects(std::vector<ObjectInstanceID> ids, PlayerColor player) const

+ 1 - 3
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;
@@ -180,7 +179,7 @@ public:
 	virtual int64_t estimateSpellDamage(const CSpell * sp, const CGHeroInstance * hero) const; //estimates damage of given spell; returns 0 if spell causes no dmg
 	virtual const CArtifactInstance * getArtInstance(ArtifactInstanceID aid) const;
 	virtual const CGObjectInstance * getObjInstance(ObjectInstanceID oid) const;
-	virtual CArtifactSet * getArtSet(const ArtifactLocation & loc) const;
+	virtual const CArtifactSet * getArtSet(const ArtifactLocation & loc) const;
 	//virtual const CGObjectInstance * getArmyInstance(ObjectInstanceID oid) const;
 
 	//objects
@@ -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;
 	}
 };
 

+ 28 - 0
lib/IGameCallback.cpp

@@ -20,11 +20,13 @@
 #include "bonuses/Propagators.h"
 #include "bonuses/Updaters.h"
 
+#include "networkPacks/ArtifactLocation.h"
 #include "serializer/CLoadFile.h"
 #include "serializer/CSaveFile.h"
 #include "rmg/CMapGenOptions.h"
 #include "mapObjectConstructors/AObjectTypeHandler.h"
 #include "mapObjectConstructors/CObjectClassesHandler.h"
+#include "mapObjects/CGMarket.h"
 #include "mapObjects/CGTownInstance.h"
 #include "mapObjects/CObjectHandler.h"
 #include "mapObjects/CQuest.h"
@@ -266,6 +268,32 @@ CArmedInstance * CNonConstInfoCallback::getArmyInstance(const ObjectInstanceID &
 	return dynamic_cast<CArmedInstance *>(getObjInstance(oid));
 }
 
+CArtifactSet * CNonConstInfoCallback::getArtSet(const ArtifactLocation & loc)
+{
+	if(auto hero = getHero(loc.artHolder))
+	{
+		if(loc.creature.has_value())
+		{
+			if(loc.creature.value() == SlotID::COMMANDER_SLOT_PLACEHOLDER)
+				return hero->commander;
+			else
+				return hero->getStackPtr(loc.creature.value());
+		}
+		else
+		{
+			return hero;
+		}
+	}
+	else if(auto market = dynamic_cast<CGArtifactsAltar*>(getObjInstance(loc.artHolder)))
+	{
+		return market;
+	}
+	else
+	{
+		return nullptr;
+	}
+}
+
 bool IGameCallback::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero)
 {
 	//only server knows

+ 3 - 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;
@@ -146,6 +146,7 @@ public:
 	using CGameInfoCallback::getTile;
 	using CGameInfoCallback::getArtInstance;
 	using CGameInfoCallback::getObjInstance;
+	using CGameInfoCallback::getArtSet;
 
 	PlayerState * getPlayerState(const PlayerColor & color, bool verbose = true);
 	TeamState * getTeam(const TeamID & teamID); //get team by team ID
@@ -156,6 +157,7 @@ public:
 	CArtifactInstance * getArtInstance(const ArtifactInstanceID & aid);
 	CGObjectInstance * getObjInstance(const ObjectInstanceID & oid);
 	CArmedInstance * getArmyInstance(const ObjectInstanceID & oid);
+	CArtifactSet * getArtSet(const ArtifactLocation & loc);
 
 	virtual void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) = 0;
 };

+ 5 - 0
lib/MetaString.cpp

@@ -325,6 +325,11 @@ void MetaString::serializeJson(JsonSerializeFormat & handler)
 		jsonDeserialize(handler.getCurrent());
 }
 
+void MetaString::appendName(const ArtifactID & id)
+{
+	appendTextID(id.toEntity(VLC)->getNameTextID());
+}
+
 void MetaString::appendName(const SpellID & id)
 {
 	appendTextID(id.toEntity(VLC)->getNameTextID());

+ 1 - 0
lib/MetaString.h

@@ -75,6 +75,7 @@ public:
 	/// Appends specified number to resulting string
 	void appendNumber(int64_t value);
 
+	void appendName(const ArtifactID& id);
 	void appendName(const SpellID& id);
 	void appendName(const PlayerColor& id);
 	void appendName(const CreatureID & id, TQuantity count);

+ 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);

+ 7 - 1
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
 	{
@@ -610,7 +612,10 @@ public:
 		CREATURE_SLOT = 0,
 		
 		// Commander
-		COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6
+		COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6,
+
+		// Altar
+		ALTAR = BACKPACK_START
 	};
 
 	static_assert(MISC5 < BACKPACK_START, "incorrect number of artifact slots");
@@ -646,6 +651,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

+ 1 - 0
lib/constants/NumericConstants.h

@@ -51,6 +51,7 @@ namespace GameConstants
 
 	constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement
 	constexpr int64_t PLAYER_RESOURCES_CAP = 1000 * 1000 * 1000;
+	constexpr int ALTAR_ARTIFACTS_SLOTS = 22;
 }
 
 VCMI_LIB_NAMESPACE_END

+ 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
 

+ 5 - 0
lib/mapObjectConstructors/CommonConstructors.cpp

@@ -238,6 +238,11 @@ CGMarket * MarketInstanceConstructor::createObject(IGameCallback * cb) const
 				return new CGUniversity(cb);
 		}
 	}
+	else if(marketModes.size() == 2)
+	{
+		if(vstd::contains(marketModes, EMarketMode::ARTIFACT_EXP))
+			return new CGArtifactsAltar(cb);
+	}
 	return new CGMarket(cb);
 }
 

+ 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
 

+ 65 - 19
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())
@@ -1261,7 +1276,7 @@ EDiggingStatus CGHeroInstance::diggingStatus() const
 {
 	if(static_cast<int>(movement) < movementPointsLimit(true))
 		return EDiggingStatus::LACK_OF_MOVEMENT;
-	if(ArtifactID(ArtifactID::GRAIL).toArtifact()->canBePutAt(this))
+	if(!ArtifactID(ArtifactID::GRAIL).toArtifact()->canBePutAt(this))
 		return EDiggingStatus::BACKPACK_IS_FULL;
 	return cb->getTileDigStatus(visitablePos());
 }
@@ -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

+ 5 - 0
lib/mapObjects/CGMarket.cpp

@@ -113,4 +113,9 @@ void CGUniversity::onHeroVisit(const CGHeroInstance * h) const
 	cb->showObjectWindow(this, EOpenWindowMode::UNIVERSITY_WINDOW, h, true);
 }
 
+ArtBearer::ArtBearer CGArtifactsAltar::bearerType() const
+{
+	return ArtBearer::ALTAR;
+}
+
 VCMI_LIB_NAMESPACE_END

+ 15 - 0
lib/mapObjects/CGMarket.h

@@ -11,6 +11,7 @@
 
 #include "CGObjectInstance.h"
 #include "IMarket.h"
+#include "../CArtHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -80,4 +81,18 @@ public:
 	}
 };
 
+class DLL_LINKAGE CGArtifactsAltar : public CGMarket, public CArtifactSet
+{
+public:
+	using CGMarket::CGMarket;
+
+	ArtBearer::ArtBearer bearerType() const override;
+
+	template <typename Handler> void serialize(Handler & h)
+	{
+		h & static_cast<CGMarket&>(*this);
+		h & static_cast<CArtifactSet&>(*this);
+	}
+};
+
 VCMI_LIB_NAMESPACE_END

+ 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;

Some files were not shown because too many files changed in this diff