2
0
Эх сурвалжийг харах

Merge branch 'beta' into sod-fly

Dydzio 1 жил өмнө
parent
commit
ad8d57f7a4
100 өөрчлөгдсөн 802 нэмэгдсэн , 337 устгасан
  1. 1 1
      .github/workflows/github.yml
  2. 3 3
      AI/BattleAI/BattleEvaluator.cpp
  3. 4 72
      AI/BattleAI/BattleExchangeVariant.cpp
  4. 0 5
      AI/BattleAI/BattleExchangeVariant.h
  5. 1 1
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  6. 1 1
      AI/VCAI/ArmyManager.cpp
  7. 1 0
      AI/VCAI/Pathfinding/AIPathfinderConfig.cpp
  8. 12 1
      CCallback.cpp
  9. 83 0
      ChangeLog.md
  10. 2 2
      Mods/vcmi/config/vcmi/english.json
  11. 26 9
      Mods/vcmi/config/vcmi/german.json
  12. 5 7
      client/Client.cpp
  13. 1 0
      client/Client.h
  14. 32 1
      client/adventureMap/AdventureMapInterface.cpp
  15. 3 0
      client/adventureMap/CInGameConsole.cpp
  16. 1 1
      client/battle/BattleActionsController.cpp
  17. 1 1
      client/battle/BattleAnimationClasses.cpp
  18. 1 1
      client/battle/BattleInterface.cpp
  19. 1 1
      client/battle/CreatureAnimation.cpp
  20. 1 1
      client/battle/CreatureAnimation.h
  21. 7 7
      client/lobby/OptionsTab.cpp
  22. 2 2
      client/lobby/OptionsTab.h
  23. 21 13
      client/lobby/OptionsTabBase.cpp
  24. 14 0
      client/render/Canvas.cpp
  25. 3 0
      client/render/Canvas.h
  26. 2 2
      client/windows/CCreatureWindow.cpp
  27. 7 0
      client/windows/settings/AdventureOptionsTab.cpp
  28. 2 0
      cmake_modules/VCMI_lib.cmake
  29. 0 7
      config/bonuses.json
  30. 0 6
      config/creatures/dungeon.json
  31. 2 0
      config/gameConfig.json
  32. 6 1
      config/schemas/settings.json
  33. 8 0
      config/widgets/settings/adventureOptionsTab.json
  34. 1 0
      config/widgets/turnOptionsTab.json
  35. 17 15
      docs/modders/Bonus/Bonus_Types.md
  36. 2 2
      docs/modders/Entities_Format/Creature_Format.md
  37. 4 0
      docs/modders/Entities_Format/Spell_Format.md
  38. 1 1
      docs/players/Game_Mechanics.md
  39. 1 1
      include/vcmi/Creature.h
  40. 1 0
      include/vcmi/spells/Spell.h
  41. 2 0
      launcher/settingsView/csettingsview_moc.cpp
  42. 2 3
      lib/BasicTypes.cpp
  43. 4 1
      lib/CCreatureHandler.cpp
  44. 25 0
      lib/CGeneralTextHandler.cpp
  45. 12 0
      lib/CGeneralTextHandler.h
  46. 5 1
      lib/CHeroHandler.cpp
  47. 8 1
      lib/CRandomGenerator.cpp
  48. 1 0
      lib/GameSettings.cpp
  49. 1 0
      lib/GameSettings.h
  50. 1 0
      lib/IGameCallback.h
  51. 7 2
      lib/JsonNode.cpp
  52. 6 6
      lib/battle/CBattleInfoCallback.cpp
  53. 1 1
      lib/battle/CBattleInfoCallback.h
  54. 4 0
      lib/bonuses/BonusCustomTypes.cpp
  55. 4 0
      lib/bonuses/BonusCustomTypes.h
  56. 0 1
      lib/bonuses/BonusEnum.h
  57. 7 7
      lib/campaign/CampaignHandler.cpp
  58. 2 2
      lib/campaign/CampaignHandler.h
  59. 5 0
      lib/campaign/CampaignState.cpp
  60. 9 3
      lib/campaign/CampaignState.h
  61. 9 5
      lib/gameState/CGameStateCampaign.cpp
  62. 7 1
      lib/gameState/TavernHeroesPool.cpp
  63. 1 1
      lib/gameState/TavernHeroesPool.h
  64. 4 4
      lib/mapObjects/CGHeroInstance.cpp
  65. 1 5
      lib/mapObjects/CGTownBuilding.cpp
  66. 9 0
      lib/mapping/CMap.cpp
  67. 1 0
      lib/mapping/CMap.h
  68. 3 10
      lib/mapping/CMapHeader.cpp
  69. 3 2
      lib/mapping/CMapHeader.h
  70. 4 0
      lib/modding/IdentifierStorage.cpp
  71. 1 1
      lib/networkPacks/NetPacksLib.cpp
  72. 2 0
      lib/networkPacks/PacksForClient.h
  73. 1 0
      lib/pathfinder/PathfinderOptions.cpp
  74. 1 0
      lib/pathfinder/PathfinderOptions.h
  75. 7 1
      lib/pathfinder/PathfindingRules.cpp
  76. 19 1
      lib/rmg/CMapGenOptions.cpp
  77. 11 0
      lib/rmg/CMapGenOptions.h
  78. 21 22
      lib/rmg/CMapGenerator.cpp
  79. 1 3
      lib/rmg/CMapGenerator.h
  80. 7 0
      lib/rmg/RmgMap.cpp
  81. 13 2
      lib/rmg/modificators/ObjectDistributor.cpp
  82. 73 0
      lib/rmg/modificators/PrisonHeroPlacer.cpp
  83. 41 0
      lib/rmg/modificators/PrisonHeroPlacer.h
  84. 12 3
      lib/rmg/modificators/QuestArtifactPlacer.cpp
  85. 3 2
      lib/rmg/modificators/QuestArtifactPlacer.h
  86. 57 26
      lib/rmg/modificators/TreasurePlacer.cpp
  87. 3 0
      lib/rmg/modificators/TreasurePlacer.h
  88. 1 1
      lib/serializer/CSerializer.h
  89. 18 1
      lib/spells/BattleSpellMechanics.cpp
  90. 7 0
      lib/spells/CSpellHandler.cpp
  91. 2 0
      lib/spells/CSpellHandler.h
  92. 0 1
      lib/spells/effects/Timed.cpp
  93. 5 2
      mapeditor/mapcontroller.cpp
  94. 15 3
      server/CGameHandler.cpp
  95. 3 2
      server/CGameHandler.h
  96. 1 0
      server/CVCMIServer.cpp
  97. 41 43
      server/battles/BattleActionProcessor.cpp
  98. 1 1
      server/battles/BattleActionProcessor.h
  99. 5 0
      server/processors/HeroPoolProcessor.cpp
  100. 1 1
      server/processors/PlayerMessageProcessor.cpp

+ 1 - 1
.github/workflows/github.yml

@@ -312,7 +312,7 @@ jobs:
           ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs
 
     - name: Upload build
-      if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' }}
+      if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' && matrix.platform != 'mingw-32' }}
       continue-on-error: true
       run: |
         if cd '${{github.workspace}}/android/vcmi-app/build/outputs/apk/daily' ; then

+ 3 - 3
AI/BattleAI/BattleEvaluator.cpp

@@ -147,7 +147,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 				(int)bestAttack.from,
 				(int)bestAttack.attack.attacker->getPosition().hex,
 				bestAttack.attack.chargeDistance,
-				bestAttack.attack.attacker->speed(0, true),
+				bestAttack.attack.attacker->getMovementRange(0),
 				bestAttack.defenderDamageReduce,
 				bestAttack.attackerDamageReduce,
 				score
@@ -225,7 +225,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		}
 	}
 
-	return BattleAction::makeDefend(stack);
+	return stack->waited() ?  BattleAction::makeDefend(stack) : BattleAction::makeWait(stack);
 }
 
 uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> start)
@@ -553,7 +553,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
 					{
 						auto original = cb->getBattle(battleID)->battleGetUnitByID(u->unitId());
-						return  !original || u->speed() != original->speed();
+						return  !original || u->getMovementRange() != original->getMovementRange();
 					});
 
 				DamageCache safeCopy = damageCache;

+ 4 - 72
AI/BattleAI/BattleExchangeVariant.cpp

@@ -297,7 +297,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 	if(targets.unreachableEnemies.empty())
 		return result;
 
-	auto speed = activeStack->speed();
+	auto speed = activeStack->getMovementRange();
 
 	if(speed == 0)
 		return result;
@@ -324,7 +324,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 
 		auto turnsToRich = (distance - 1) / speed + 1;
 		auto hexes = closestStack->getSurroundingHexes();
-		auto enemySpeed = closestStack->speed();
+		auto enemySpeed = closestStack->getMovementRange();
 		auto speedRatio = speed / static_cast<float>(enemySpeed);
 		auto multiplier = speedRatio > 1 ? 1 : speedRatio;
 
@@ -684,11 +684,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 	for(auto hex : hexes)
 		reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex);
 
-	if(!ap.attack.shooting)
-	{
-		v.adjustPositions(melleeAttackers, ap, reachabilityMap);
-	}
-
 #if BATTLE_TRACE_LEVEL>=1
 	logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce);
 #endif
@@ -696,69 +691,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 	return v.getScore();
 }
 
-void BattleExchangeVariant::adjustPositions(
-	std::vector<const battle::Unit*> attackers,
-	const AttackPossibility & ap,
-	std::map<BattleHex, battle::Units> & reachabilityMap)
-{
-	auto hexes = ap.attack.defender->getSurroundingHexes();
-
-	boost::sort(attackers, [&](const battle::Unit * u1, const battle::Unit * u2) -> bool
-		{
-			if(attackerValue[u1->unitId()].isRetalitated && !attackerValue[u2->unitId()].isRetalitated)
-				return true;
-
-			if(attackerValue[u2->unitId()].isRetalitated && !attackerValue[u1->unitId()].isRetalitated)
-				return false;
-
-			return attackerValue[u1->unitId()].value > attackerValue[u2->unitId()].value;
-		});
-
-	vstd::erase_if_present(hexes, ap.from);
-	vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos));
-
-	float notRealizedDamage = 0;
-
-	for(auto unit : attackers)
-	{
-		if(unit->unitId() == ap.attack.attacker->unitId())
-			continue;
-
-		if(!vstd::contains_if(hexes, [&](BattleHex h) -> bool
-			{
-				return vstd::contains(reachabilityMap[h], unit);
-			}))
-		{
-			notRealizedDamage += attackerValue[unit->unitId()].value;
-			continue;
-		}
-
-		auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> float
-			{
-				auto score = vstd::contains(reachabilityMap[h], unit)
-					? reachabilityMap[h].size()
-					: 0;
-
-				if(unit->doubleWide())
-				{
-					auto backHex = unit->occupiedHex(h);
-
-					if(vstd::contains(hexes, backHex))
-						score += reachabilityMap[backHex].size();
-				}
-
-				return score;
-			});
-
-		hexes.erase(desiredPosition);
-	}
-
-	if(notRealizedDamage > ap.attackValue() && notRealizedDamage > attackerValue[ap.attack.attacker->unitId()].value)
-	{
-		dpsScore = BattleScore(EvaluationResult::INEFFECTIVE_SCORE, 0);
-	}
-}
-
 bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap)
 {
 	for(auto pos : ap.attack.attacker->getSurroundingHexes())
@@ -821,7 +753,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
 				continue;
 			}
 
-			auto unitSpeed = unit->speed(turn);
+			auto unitSpeed = unit->getMovementRange(turn);
 			auto radius = unitSpeed * (turn + 1);
 
 			ReachabilityInfo unitReachability = vstd::getOrCompute(
@@ -887,7 +819,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
 			float ratio = blockedUnitDamage / (float)(blockedUnitDamage + activeUnitDamage + 0.01);
 
 			auto unitReachability = turnBattle.getReachability(unit);
-			auto unitSpeed = unit->speed(turn); // Cached value, to avoid performance hit
+			auto unitSpeed = unit->getMovementRange(turn); // Cached value, to avoid performance hit
 
 			for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
 			{

+ 0 - 5
AI/BattleAI/BattleExchangeVariant.h

@@ -106,11 +106,6 @@ public:
 
 	const BattleScore & getScore() const { return dpsScore; }
 
-	void adjustPositions(
-		std::vector<const battle::Unit *> attackers,
-		const AttackPossibility & ap,
-		std::map<BattleHex, battle::Units> & reachabilityMap);
-
 private:
 	BattleScore dpsScore;
 	std::map<uint32_t, AttackerValue> attackerValue;

+ 1 - 1
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -117,7 +117,7 @@ std::vector<SlotInfo>::iterator ArmyManager::getWeakestCreature(std::vector<Slot
 		if(left.creature->getLevel() != right.creature->getLevel())
 			return left.creature->getLevel() < right.creature->getLevel();
 		
-		return left.creature->speed() > right.creature->speed();
+		return left.creature->getMovementRange() > right.creature->getMovementRange();
 	});
 
 	return weakest;

+ 1 - 1
AI/VCAI/ArmyManager.cpp

@@ -63,7 +63,7 @@ std::vector<SlotInfo>::iterator ArmyManager::getWeakestCreature(std::vector<Slot
 		if(left.creature->getLevel() != right.creature->getLevel())
 			return left.creature->getLevel() < right.creature->getLevel();
 		
-		return left.creature->speed() > right.creature->speed();
+		return left.creature->getMovementRange() > right.creature->getMovementRange();
 	});
 
 	return weakest;

+ 1 - 0
AI/VCAI/Pathfinding/AIPathfinderConfig.cpp

@@ -41,6 +41,7 @@ namespace AIPathfinding
 		std::shared_ptr<AINodeStorage> nodeStorage)
 		:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), hero(nodeStorage->getHero())
 	{
+		options.ignoreGuards = false;
 		options.useEmbarkAndDisembark = true;
 		options.useTeleportTwoWay = true;
 		options.useTeleportOneWay = true;

+ 12 - 1
CCallback.cpp

@@ -405,7 +405,10 @@ std::optional<BattleAction> CBattleCallback::makeSurrenderRetreatDecision(const
 
 std::shared_ptr<CPlayerBattleCallback> CBattleCallback::getBattle(const BattleID & battleID)
 {
-	return activeBattles.at(battleID);
+	if (activeBattles.count(battleID))
+		return activeBattles.at(battleID);
+
+	throw std::runtime_error("Failed to find battle " + std::to_string(battleID.getNum()) + " of player " + player->toString() + ". Number of ongoing battles: " + std::to_string(activeBattles.size()));
 }
 
 std::optional<PlayerColor> CBattleCallback::getPlayerID() const
@@ -415,10 +418,18 @@ std::optional<PlayerColor> CBattleCallback::getPlayerID() const
 
 void CBattleCallback::onBattleStarted(const IBattleInfo * info)
 {
+	if (activeBattles.count(info->getBattleID()) > 0)
+		throw std::runtime_error("Player " + player->toString() + " is already engaged in battle " + std::to_string(info->getBattleID().getNum()));
+
+	logGlobal->debug("Battle %d started for player %s", info->getBattleID(), player->toString());
 	activeBattles[info->getBattleID()] = std::make_shared<CPlayerBattleCallback>(info, *getPlayerID());
 }
 
 void CBattleCallback::onBattleEnded(const BattleID & battleID)
 {
+	if (activeBattles.count(battleID) == 0)
+		throw std::runtime_error("Player " + player->toString() + " is not engaged in battle " + std::to_string(battleID.getNum()));
+
+	logGlobal->debug("Battle %d ended for player %s", battleID, player->toString());
 	activeBattles.erase(battleID);
 }

+ 83 - 0
ChangeLog.md

@@ -1,3 +1,86 @@
+# 1.4.2 -> 1.4.3
+
+### General
+* Fixed the synchronisation of the audio and video of the opening movies.
+* Fixed a bug that caused spells from mods to not show up in the Mage's Guild.
+* Changed the default SDL driver on Windows from opengl to autodetection
+* When a hero visits a town with a garrisoned hero, they will now automatically exchange spells if one of them has the Scholar skill.
+* Movement and mana points are now replenished for new heroes in taverns.
+
+### Multiplayer
+* Simturn contact detection will now correctly check for hero moving range
+* Simturn contact detection will now ignore wandering monsters
+* Right-clicking the Simturns AI option now displays a tooltip
+* Interaction attempts with other players during simturns will now have more concise error messages
+* Turn timers are now limited to 24 hours in order to prevent bugs caused by an integer overflow.
+* Fixed delays when editing turn timer duration
+* Ending a turn during simturns will now block the interface correctly.
+
+### Campaigns
+* Player will no longer start the United Front of Song for the Father campaign with two Nimbuses.
+* Fixed missing campaign description after loading saved game
+* Campaign completion checkmarks will now be displayed after the entire campaign has been completed, rather than just after the first scenario.
+* Fixed positioning of prologue and epilogue text during campaign scenario intros
+
+### Interface
+* Fixed switching between pages on small version of spellbook
+* Saves with long filenames are now truncated in the UI to prevent overflow.
+* Added option to sort saved games by change date
+* Game now shows correct resource when selecting start bonus
+* It is now possible to inspect commander skills during battles.
+* Fixed incorrect cursor being displayed when hovering over navigable water tiles
+* Fixed incorrect cursor display when hovering over water objects accessible from shore
+
+### Stability
+* Fixed possible creation of a duplicate hero in a random map when the player has chosen the starting hero.
+* Fixed a crash when using the 'vcmiobelisk' cheat more than once.
+* Fixed crash when reaching level 201. The maximum level is now limited to 197.
+* Fixed crash when accessing a spell with an invalid SPELLCASTER bonus
+* Fixed crash when trying to play music for an inaccessible tile 
+* Fixed memory corruption on loading of old mods with illegal 'index' field
+* Fixed possible crash on server shutdown on Android
+* Fixed possible crash when the affinity of the hero class is set to an invalid value
+* Fixed crash on invalid creature in hero army due to outdated or broken mods
+* Failure to initialise video subsystem now displays error message instead of silent crash
+
+### Battles
+* Battle turn queue now displays current turn
+* Added option to show unit statistics sidebar in battle
+* Right-clicking on a unit in the battle turn queue now displays the unit details popup.
+* Fixed error messages for SUMMON_GUARDIANS and TRANSMUTATION bonuses
+* Fixed Dendroid Bind ability
+* Black Dragons no longer hate Giants, only Titans
+* Spellcasting units such as Archangels can no longer cast spells on themselves.
+* Coronius specialty will now correctly select affected units
+
+### Launcher
+* Welcome screen will automatically detect existing Heroes 3 installation on Windows
+* It is now possible to install mods by dragging and dropping onto the launcher.
+* It is now possible to install maps and campaigns by dragging and dropping onto the launcher.
+* Czech launcher translation added
+* Added option to select preferred SDL driver in launcher
+
+### Map Editor
+* Fixed saving of allowed abilities, spells, artifacts or heroes
+
+### AI
+* AI will no longer attempt to move immobilized units, such as those under the effect of Dendroid Bind.
+* Fixed shooters not shooting when they have a range penalty
+* Fixed Fire Elemental spell casting
+* Fixed rare bug where unit would sometimes do nothing in battle
+
+### Modding
+* Added better reporting of "invalid identifiers" errors with suggestions on how to fix them
+* Added FEROCITY bonus (HotA Aysiud)
+* Added ENEMY_ATTACK_REDUCTION bonus (HotA Nix)
+* Added REVENGE bonus (HotA Haspid)
+* Extended DEATH_STARE bonus to support Pirates ability (HotA)
+* DEATH_STARE now supports spell ID in addInfo field to override used spell
+* SPELL_BEFORE_ATTACK bonus now supports spell priorities
+* FIRST_STRIKE bonus supports subtypes damageTypeMelee, damageTypeRanged and damageTypeAll
+* BLOCKS_RETALIATION now also blocks FIRST_STRIKE bonus
+* Added 'canCastOnSelf' field for spells to allow creatures to cast spells on themselves.
+
 # 1.4.1 -> 1.4.2
 
 ### General

+ 2 - 2
Mods/vcmi/config/vcmi/english.json

@@ -145,6 +145,8 @@
 	"vcmi.adventureOptions.mapScrollSpeed1.help": "Set the map scrolling speed to very slow.",
 	"vcmi.adventureOptions.mapScrollSpeed5.help": "Set the map scrolling speed to very fast.",
 	"vcmi.adventureOptions.mapScrollSpeed6.help": "Set the map scrolling speed to instantaneous.",
+	"vcmi.adventureOptions.hideBackground.hover" : "Hide Background",
+	"vcmi.adventureOptions.hideBackground.help" : "{Hide Background}\n\nHide the adventuremap in the background and show a texture instead.",
 
 	"vcmi.battleOptions.queueSizeLabel.hover": "Show Turn Order Queue",
 	"vcmi.battleOptions.queueSizeNoneButton.hover": "OFF",
@@ -333,8 +335,6 @@
 	"vcmi.stackExperience.rank.9" : "Master",
 	"vcmi.stackExperience.rank.10" : "Ace",
 
-	"core.bonus.ACCURATE_SHOT.name": "Accurate Shot",
-	"core.bonus.ACCURATE_SHOT.description": "Has (${val}% - penalties) extra kills chance",
 	"core.bonus.ADDITIONAL_ATTACK.name": "Double Strike",
 	"core.bonus.ADDITIONAL_ATTACK.description": "Attacks twice",
 	"core.bonus.ADDITIONAL_RETALIATION.name": "Additional retaliations",

+ 26 - 9
Mods/vcmi/config/vcmi/german.json

@@ -72,6 +72,7 @@
 	"vcmi.lobby.noUnderground" : "Kein Untergrund",
 	"vcmi.lobby.sortDate" : "Ordnet Karten nach Änderungsdatum",
 
+	"vcmi.client.errors.missingCampaigns" : "{Fehlende Dateien}\n\nEs wurden keine Kampagnendateien gefunden! Möglicherweise verwendest du unvollständige oder beschädigte Heroes 3 Datendateien. Bitte installiere die Spieldaten neu.",
 	"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
 	"vcmi.server.errors.modsToEnable"    : "{Erforderliche Mods um das Spiel zu laden}",
 	"vcmi.server.errors.modsToDisable"   : "{Folgende Mods müssen deaktiviert werden}",
@@ -136,12 +137,16 @@
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen",
 	"vcmi.adventureOptions.smoothDragging.hover" : "Nahtloses Ziehen der Karte",
 	"vcmi.adventureOptions.smoothDragging.help" : "{Nahtloses Ziehen der Karte}\n\nWenn aktiviert hat das Ziehen der Karte einen sanften Auslaufeffekt.",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Fading-Effekte überspringen",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Fading-Effekte überspringen}\n\nWenn diese Funktion aktiviert ist, werden das Ausblenden von Objekten und ähnliche Effekte übersprungen (Ressourcensammlung, Anlegen von Schiffen usw.). Macht die Benutzeroberfläche in einigen Fällen auf Kosten der Ästhetik reaktiver. Besonders nützlich in PvP-Spielen. Für maximale Bewegungsgeschwindigkeit ist das Überspringen unabhängig von dieser Einstellung aktiv.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed1.help": "Geschwindigkeit des Kartenbildlaufs auf sehr langsam einstellen",
 	"vcmi.adventureOptions.mapScrollSpeed5.help": "Geschwindigkeit des Kartenbildlaufs auf sehr schnell einstellen",
 	"vcmi.adventureOptions.mapScrollSpeed6.help": "Geschwindigkeit des Kartenbildlaufs auf sofort einstellen",
+	"vcmi.adventureOptions.hideBackground.hover" : "Hintergrund ausblenden",
+	"vcmi.adventureOptions.hideBackground.help" : "{Hintergrund ausblenden}\n\nDie Abenteuerkarte im Hintergrund ausblenden und stattdessen eine Textur anzeigen.",
 
 	"vcmi.battleOptions.queueSizeLabel.hover": "Reihenfolge der Kreaturen anzeigen",
 	"vcmi.battleOptions.queueSizeNoneButton.hover": "AUS",
@@ -182,6 +187,9 @@
 	"vcmi.battleWindow.damageEstimation.kills" : "%d werden verenden",
 	"vcmi.battleWindow.damageEstimation.kills.1" : "%d werden verenden",
 	"vcmi.battleWindow.killed" : "Getötet",
+	"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.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen",
 
@@ -244,18 +252,29 @@
 
 	"vcmi.optionsTab.turnOptions.hover" : "Spielzug-Optionen",
 	"vcmi.optionsTab.turnOptions.help" : "Optionen zu Spielzug-Timer und simultanen Zügen",
+	"vcmi.optionsTab.selectPreset" : "Voreinstellung",
 
 	"vcmi.optionsTab.chessFieldBase.hover" : "Basis-Timer",
 	"vcmi.optionsTab.chessFieldTurn.hover" : "Spielzug-Timer",
 	"vcmi.optionsTab.chessFieldBattle.hover" : "Kampf-Timer",
 	"vcmi.optionsTab.chessFieldUnit.hover" : "Einheiten-Timer",
+	"vcmi.optionsTab.chessFieldBase.help" : "Wird verwendet, wenn {Spielzug-Timer} 0 erreicht. Wird einmal zu Beginn des Spiels gesetzt. Bei Erreichen von Null wird der aktuelle Zug beendet. Jeder laufende Kampf endet mit einem Verlust.",
 	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Reste werden am Ende der Runde zum {Basis-Timer} hinzugefügt.",
 	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Jede nicht verbrauchte Zeit ist verloren",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Wird in Kämpfen mit der KI oder im PvP-Kampf verwendet, wenn der {Einheiten-Timer} abläuft. Wird zu Beginn eines jeden Kampfes zurückgesetzt.",
 	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Der Rest wird am Ende des Zuges der Einheit zum {Kampf-Timer} hinzugefügt.",
 	"vcmi.optionsTab.chessFieldUnitDiscard.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Wird zu Beginn des Zuges jeder Einheit zurückgesetzt. Jede nicht verbrauchte Zeit ist verloren",
 
 	"vcmi.optionsTab.accumulate" : "Akkumulieren",
 
+	"vcmi.optionsTab.simturnsTitle" : "Simultane Züge",
+	"vcmi.optionsTab.simturnsMin.hover" : "Zumindest für",
+	"vcmi.optionsTab.simturnsMax.hover" : "Höchstens für",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Experimentell) Simultane KI Züge",
+	"vcmi.optionsTab.simturnsMin.help" : "Spielt gleichzeitig für eine bestimmte Anzahl von Tagen. Die Kontakte zwischen den Spielern sind während dieser Zeit blockiert",
+	"vcmi.optionsTab.simturnsMax.help" : "Spielt gleichzeitig für eine bestimmte Anzahl von Tagen oder bis zum Kontakt mit einem anderen Spieler",
+	"vcmi.optionsTab.simturnsAI.help" : "{Simultane KI Züge}\nExperimentelle Option. Ermöglicht es den KI-Spielern, gleichzeitig mit dem menschlichen Spieler zu agieren, wenn simultane Spielzüge aktiviert sind.",
+
 	"vcmi.optionsTab.turnTime.select"     : "Spielzug-Timer-Voreinst. wählen",
 	"vcmi.optionsTab.turnTime.unlimited"  : "Unbegrenzter Spielzug-Timer",
 	"vcmi.optionsTab.turnTime.classic.1"  : "Klassischer Timer: 1 Minute",
@@ -280,14 +299,6 @@
 	"vcmi.optionsTab.simturns.blocked1"       : "Simzüge: 1 Woche, Kontakte block.",
 	"vcmi.optionsTab.simturns.blocked2"       : "Simzüge: 2 Wochen, Kontakte block.",
 	"vcmi.optionsTab.simturns.blocked4"       : "Simzüge: 1 Monat, Kontakte block.",
-
-	"vcmi.optionsTab.simturnsTitle" : "Simultane Züge",
-	"vcmi.optionsTab.simturnsMin.hover" : "Zumindest für",
-	"vcmi.optionsTab.simturnsMax.hover" : "Höchstens für",
-	"vcmi.optionsTab.simturnsAI.hover" : "(Experimentell) Simultane KI Züge",
-	"vcmi.optionsTab.simturnsMin.help" : "Spielt gleichzeitig für eine bestimmte Anzahl von Tagen. Die Kontakte zwischen den Spielern sind während dieser Zeit blockiert",
-	"vcmi.optionsTab.simturnsMax.help" : "Spielt gleichzeitig für eine bestimmte Anzahl von Tagen oder bis zum Kontakt mit einem anderen Spieler",
-	"vcmi.optionsTab.simturnsAI.help" : "{Simultane KI Züge}\nExperimentelle Option. Ermöglicht es den KI-Spielern, gleichzeitig mit dem menschlichen Spieler zu agieren, wenn simultane Spielzüge aktiviert sind.",
 	
 	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
 	// Using this information, VCMI will automatically select correct plural form for every possible amount
@@ -323,7 +334,7 @@
 	"vcmi.stackExperience.rank.8" : "Elite",
 	"vcmi.stackExperience.rank.9" : "Meister",
 	"vcmi.stackExperience.rank.10" : "Ass",
-	
+
 	"core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag",
 	"core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an",
 	"core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen",
@@ -362,6 +373,8 @@
 	"core.bonus.ENCHANTER.description": "Kann jede Runde eine Masse von ${subtype.spell} zaubern",
 	"core.bonus.ENCHANTED.name": "Verzaubert",
 	"core.bonus.ENCHANTED.description": "Beeinflusst von permanentem ${subtype.spell}",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name": "Angriff ignorieren (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description": "Bei Angriff, wird ${val}% des Angreifers ignoriert.",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoriere Verteidigung (${val}%)",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoriert einen Teil der Verteidigung für den Angriff",
 	"core.bonus.FIRE_IMMUNITY.name": "Feuerimmunität",
@@ -374,6 +387,8 @@
 	"core.bonus.FEAR.description": "Verursacht Furcht bei einem gegnerischen Stapel",
 	"core.bonus.FEARLESS.name": "Furchtlos",
 	"core.bonus.FEARLESS.description": "immun gegen die Fähigkeit Furcht",
+	"core.bonus.FEROCITY.name": "Wildheit",
+	"core.bonus.FEROCITY.description": "Greift ${val} zusätzliche Male an, wenn jemand getötet wird",
 	"core.bonus.FLYING.name": "Fliegen",
 	"core.bonus.FLYING.description": "Kann fliegen (ignoriert Hindernisse)",
 	"core.bonus.FREE_SHOOTING.name": "Nah schießen",
@@ -428,6 +443,8 @@
 	"core.bonus.REBIRTH.description": "${val}% des Stacks wird nach dem Tod auferstehen",
 	"core.bonus.RETURN_AFTER_STRIKE.name": "Angriff und Rückkehr",
 	"core.bonus.RETURN_AFTER_STRIKE.description": "Kehrt nach Nahkampfangriff zurück",
+	"core.bonus.REVENGE.name": "Rache",
+	"core.bonus.REVENGE.description": "Verursacht zusätzlichen Schaden basierend auf der verlorenen Gesundheit des Angreifers im Kampf",
 	"core.bonus.SHOOTER.name": "Fernkämpfer",
 	"core.bonus.SHOOTER.description": "Kreatur kann schießen",
 	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Schießt rundherum",

+ 5 - 7
client/Client.cpp

@@ -560,18 +560,16 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player)
 
 void CClient::battleStarted(const BattleInfo * info)
 {
+	std::shared_ptr<CPlayerInterface> att, def;
+	auto & leftSide = info->sides[0];
+	auto & rightSide = info->sides[1];
+
 	for(auto & battleCb : battleCallbacks)
 	{
-		if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; })
-			|| !battleCb.first.isValidPlayer())
-		{
+		if(!battleCb.first.isValidPlayer() || battleCb.first == leftSide.color || battleCb.first == rightSide.color)
 			battleCb.second->onBattleStarted(info);
-		}
 	}
 
-	std::shared_ptr<CPlayerInterface> att, def;
-	auto & leftSide = info->sides[0], & rightSide = info->sides[1];
-
 	//If quick combat is not, do not prepare interfaces for battleint
 	auto callBattleStart = [&](PlayerColor color, ui8 side)
 	{

+ 1 - 0
client/Client.h

@@ -202,6 +202,7 @@ public:
 	bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;};
 	void giveHeroBonus(GiveBonus * bonus) override {};
 	void setMovePoints(SetMovePoints * smp) override {};
+	void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override {};
 	void setManaPoints(ObjectInstanceID hid, int val) override {};
 	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {};
 	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {};

+ 32 - 1
client/adventureMap/AdventureMapInterface.cpp

@@ -31,6 +31,7 @@
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
 #include "../render/Canvas.h"
+#include "../render/IRenderHandler.h"
 #include "../CMT.h"
 #include "../PlayerLocalState.h"
 #include "../CPlayerInterface.h"
@@ -168,6 +169,15 @@ void AdventureMapInterface::show(Canvas & to)
 
 void AdventureMapInterface::dim(Canvas & to)
 {
+	if(settings["adventure"]["hideBackground"].Bool())
+		for (auto window : GH.windows().findWindows<IShowActivatable>())
+		{
+			if(!std::dynamic_pointer_cast<AdventureMapInterface>(window) && std::dynamic_pointer_cast<CIntObject>(window) && std::dynamic_pointer_cast<CIntObject>(window)->pos.w >= 800 && std::dynamic_pointer_cast<CIntObject>(window)->pos.w >= 600)
+			{
+				to.fillTexture(GH.renderHandler().loadImage(ImagePath::builtin("DiBoxBck")));
+				return;
+			}
+		}
 	for (auto window : GH.windows().findWindows<IShowActivatable>())
 	{
 		if (!std::dynamic_pointer_cast<AdventureMapInterface>(window) && !std::dynamic_pointer_cast<RadialMenu>(window) && !window->isPopupWindow())
@@ -467,6 +477,18 @@ void AdventureMapInterface::hotkeyEndingTurn()
 	LOCPLINT->cb->endTurn();
 
 	mapAudio->onPlayerTurnEnded();
+
+	// Normally, game will receive PlayerStartsTurn call almost instantly with new player ID that will switch UI to waiting mode
+	// However, when simturns are active it is possible for such call not to come because another player is still acting
+	// So find first player other than ours that is acting at the moment and update UI as if he had started turn
+	for (auto player = PlayerColor(0); player < PlayerColor::PLAYER_LIMIT; ++player)
+	{
+		if (player != LOCPLINT->playerID && LOCPLINT->cb->isPlayerMakingTurn(player))
+		{
+			onEnemyTurnStarted(player, LOCPLINT->cb->getStartInfo()->playerInfos.at(player).isControlledByHuman());
+			break;
+		}
+	}
 }
 
 const CGObjectInstance* AdventureMapInterface::getActiveObject(const int3 &mapPos)
@@ -679,7 +701,7 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
 			if(pathNode->layer == EPathfindingLayer::LAND)
 				CCS->curh->set(cursorMove[turns]);
 			else
-				CCS->curh->set(cursorSailVisit[turns]);
+				CCS->curh->set(cursorSail[turns]);
 			break;
 
 		case EPathNodeAction::VISIT:
@@ -694,6 +716,15 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
 			}
 			else if(pathNode->layer == EPathfindingLayer::LAND)
 				CCS->curh->set(cursorVisit[turns]);
+			else if (pathNode->layer == EPathfindingLayer::SAIL &&
+					 objAtTile &&
+					 objAtTile->isCoastVisitable() &&
+					 pathNode->theNodeBefore &&
+					 pathNode->theNodeBefore->layer == EPathfindingLayer::LAND )
+			{
+				// exception - when visiting shipwreck located on coast from land - show 'horse' cursor, not 'ship' cursor
+				CCS->curh->set(cursorVisit[turns]);
+			}
 			else
 				CCS->curh->set(cursorSailVisit[turns]);
 			break;

+ 3 - 0
client/adventureMap/CInGameConsole.cpp

@@ -243,6 +243,9 @@ void CInGameConsole::startEnteringText()
 	if (!isActive())
 		return;
 
+	if(enteredText != "")
+		return;
+		
 	assert(currentStatusBar.expired());//effectively, nullptr check
 
 	currentStatusBar = GH.statusbar();

+ 1 - 1
client/battle/BattleActionsController.cpp

@@ -568,7 +568,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 	switch (action.get())
 	{
 		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
-			return (targetStack && targetStackOwned && targetStack->speed() > 0);
+			return (targetStack && targetStackOwned && targetStack->getMovementRange() > 0);
 
 		case PossiblePlayerBattleAction::CREATURE_INFO:
 			return (targetStack && targetStackOwned && targetStack->alive());

+ 1 - 1
client/battle/BattleAnimationClasses.cpp

@@ -363,7 +363,7 @@ bool MovementAnimation::init()
 	Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack);
 	Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack);
 
-	progressPerSecond = AnimationControls::getMovementDistance(stack->unitType());
+	progressPerSecond = AnimationControls::getMovementRange(stack->unitType());
 
 	begX = begPosition.x;
 	begY = begPosition.y;

+ 1 - 1
client/battle/BattleInterface.cpp

@@ -640,7 +640,7 @@ void BattleInterface::tacticPhaseEnd()
 
 static bool immobile(const CStack *s)
 {
-	return !s->speed(0, true); //should bound stacks be immobile?
+	return s->getMovementRange() == 0; //should bound stacks be immobile?
 }
 
 void BattleInterface::tacticNextStack(const CStack * current)

+ 1 - 1
client/battle/CreatureAnimation.cpp

@@ -148,7 +148,7 @@ float AnimationControls::getSpellEffectSpeed()
 	return static_cast<float>(getAnimationSpeedFactor() * 10);
 }
 
-float AnimationControls::getMovementDistance(const CCreature * creature)
+float AnimationControls::getMovementRange(const CCreature * creature)
 {
 	// H3 speed: 2/4/6 tiles per second
 	return static_cast<float>( 2.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime);

+ 1 - 1
client/battle/CreatureAnimation.h

@@ -50,7 +50,7 @@ namespace AnimationControls
 	float getSpellEffectSpeed();
 
 	/// returns speed of movement animation across the screen, in tiles per second
-	float getMovementDistance(const CCreature * creature);
+	float getMovementRange(const CCreature * creature);
 
 	/// returns speed of movement animation across the screen, in pixels per seconds
 	float getFlightDistance(const CCreature * creature);

+ 7 - 7
client/lobby/OptionsTab.cpp

@@ -400,12 +400,11 @@ void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow()
 	textBonusDescription = std::make_shared<CTextBox>(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 }
 
-OptionsTab::SelectionWindow::SelectionWindow(CPlayerSettingsHelper & helper, SelType _type)
-	: CWindowObject(BORDERED), CPlayerSettingsHelper(helper)
+OptionsTab::SelectionWindow::SelectionWindow(const PlayerColor & color, SelType _type)
+	: CWindowObject(BORDERED), color(color)
 {
 	addUsedEvents(LCLICK | SHOW_POPUP);
 
-	color = helper.playerSettings.color;
 	type = _type;
 
 	initialFaction = SEL->getStartInfo()->playerInfos.find(color)->second.castle;
@@ -481,9 +480,10 @@ void OptionsTab::SelectionWindow::setSelection()
 
 void OptionsTab::SelectionWindow::reopen()
 {
-	std::shared_ptr<SelectionWindow> window = std::shared_ptr<SelectionWindow>(new SelectionWindow(*this, type));
+	std::shared_ptr<SelectionWindow> window = std::shared_ptr<SelectionWindow>(new SelectionWindow(color, type));
 	close();
-	GH.windows().pushWindow(window);
+	if(CSH->isMyColor(color) || CSH->isHost())
+		GH.windows().pushWindow(window);
 }
 
 void OptionsTab::SelectionWindow::recreate()
@@ -632,7 +632,7 @@ void OptionsTab::SelectionWindow::genContentHeroes()
 
 void OptionsTab::SelectionWindow::genContentBonus()
 {
-	PlayerSettings set = PlayerSettings(playerSettings);
+	PlayerSettings set = SEL->getStartInfo()->playerInfos.find(color)->second;
 
 	int i = 0;
 	for(auto elem : allowedBonus)
@@ -819,7 +819,7 @@ void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition)
 		return;
 
 	GH.input().hapticFeedback();
-	GH.windows().createAndPushWindow<SelectionWindow>(*this, selectionType);
+	GH.windows().createAndPushWindow<SelectionWindow>(playerSettings.color, selectionType);
 }
 
 void OptionsTab::SelectedBox::scrollBy(int distance)

+ 2 - 2
client/lobby/OptionsTab.h

@@ -96,7 +96,7 @@ private:
 		CPlayerOptionTooltipBox(CPlayerSettingsHelper & helper);
 	};
 
-	class SelectionWindow : public CWindowObject, public CPlayerSettingsHelper
+	class SelectionWindow : public CWindowObject
 	{
 		//const int ICON_SMALL_WIDTH = 48;
 		const int ICON_SMALL_HEIGHT = 32;
@@ -148,7 +148,7 @@ private:
 	public:
 		void reopen();
 
-		SelectionWindow(CPlayerSettingsHelper & helper, SelType _type);
+		SelectionWindow(const PlayerColor & color, SelType _type);
 	};
 
 	/// Image with current town/hero/bonus

+ 21 - 13
client/lobby/OptionsTabBase.cpp

@@ -22,6 +22,14 @@
 #include "../../lib/MetaString.h"
 #include "../../lib/CGeneralTextHandler.h"
 
+static std::string timeToString(int time)
+{
+	std::stringstream ss;
+	ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60;
+	return ss.str();
+};
+
+
 std::vector<TurnTimerInfo> OptionsTabBase::getTimerPresets() const
 {
 	std::vector<TurnTimerInfo> result;
@@ -141,43 +149,51 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
 		else if(l.empty())
 			return sec;
 
-		return std::stoi(l) * 60 + std::stoi(r);
+		return std::min(24*60, std::stoi(l)) * 60 + std::stoi(r);
 	};
 
-	addCallback("parseAndSetTimer_base", [parseTimerString](const std::string & str){
+	addCallback("parseAndSetTimer_base", [this, parseTimerString](const std::string & str){
 		int time = parseTimerString(str) * 1000;
 		if(time >= 0)
 		{
 			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
 			tinfo.baseTimer = time;
 			CSH->setTurnTimerInfo(tinfo);
+			if(auto ww = widget<CTextInput>("chessFieldBase"))
+				ww->setText(timeToString(time), false);
 		}
 	});
-	addCallback("parseAndSetTimer_turn", [parseTimerString](const std::string & str){
+	addCallback("parseAndSetTimer_turn", [this, parseTimerString](const std::string & str){
 		int time = parseTimerString(str) * 1000;
 		if(time >= 0)
 		{
 			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
 			tinfo.turnTimer = time;
 			CSH->setTurnTimerInfo(tinfo);
+			if(auto ww = widget<CTextInput>("chessFieldTurn"))
+				ww->setText(timeToString(time), false);
 		}
 	});
-	addCallback("parseAndSetTimer_battle", [parseTimerString](const std::string & str){
+	addCallback("parseAndSetTimer_battle", [this, parseTimerString](const std::string & str){
 		int time = parseTimerString(str) * 1000;
 		if(time >= 0)
 		{
 			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
 			tinfo.battleTimer = time;
 			CSH->setTurnTimerInfo(tinfo);
+			if(auto ww = widget<CTextInput>("chessFieldBattle"))
+				ww->setText(timeToString(time), false);
 		}
 	});
-	addCallback("parseAndSetTimer_unit", [parseTimerString](const std::string & str){
+	addCallback("parseAndSetTimer_unit", [this, parseTimerString](const std::string & str){
 		int time = parseTimerString(str) * 1000;
 		if(time >= 0)
 		{
 			TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo;
 			tinfo.unitTimer = time;
 			CSH->setTurnTimerInfo(tinfo);
+			if(auto ww = widget<CTextInput>("chessFieldUnit"))
+				ww->setText(timeToString(time), false);
 		}
 	});
 
@@ -359,14 +375,6 @@ void OptionsTabBase::recreate()
 		}
 	}
 
-	//chess timer
-	auto timeToString = [](int time) -> std::string
-	{
-		std::stringstream ss;
-		ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60;
-		return ss.str();
-	};
-
 	if(auto ww = widget<CTextInput>("chessFieldBase"))
 		ww->setText(timeToString(turnTimerRemote.baseTimer), false);
 	if(auto ww = widget<CTextInput>("chessFieldTurn"))

+ 14 - 0
client/render/Canvas.cpp

@@ -182,6 +182,20 @@ void Canvas::drawColorBlended(const Rect & target, const ColorRGBA & color)
 	CSDL_Ext::fillRectBlended(surface, realTarget, CSDL_Ext::toSDL(color));
 }
 
+void Canvas::fillTexture(const std::shared_ptr<IImage>& image)
+{
+	assert(image);
+	if (!image)
+		return;
+		
+	Rect imageArea = Rect(Point(0, 0), image->dimensions());
+	for (int y=0; y < surface->h; y+= imageArea.h)
+	{
+		for (int x=0; x < surface->w; x+= imageArea.w)
+			image->draw(surface, renderArea.x + x, renderArea.y + y);
+	}
+}
+
 SDL_Surface * Canvas::getInternalSurface()
 {
 	return surface;

+ 3 - 0
client/render/Canvas.h

@@ -99,6 +99,9 @@ public:
 	/// fills selected area with blended color
 	void drawColorBlended(const Rect & target, const ColorRGBA & color);
 
+	/// fills canvas with texture
+	void fillTexture(const std::shared_ptr<IImage>& image);
+
 	/// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished.
 	SDL_Surface * getInternalSurface();
 

+ 2 - 2
client/windows/CCreatureWindow.cpp

@@ -543,7 +543,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s
 		addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(battleStack->isShooter()), battleStack->getDefense(battleStack->isShooter()));
 		addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(battleStack->isShooter()) * dmgMultiply, battleStack->getMaxDamage(battleStack->isShooter()) * dmgMultiply);
 		addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), battleStack->getMaxHealth());
-		addStatLabel(EStat::SPEED, parent->info->creature->speed(), battleStack->speed());
+		addStatLabel(EStat::SPEED, parent->info->creature->getMovementRange(), battleStack->getMovementRange());
 
 		if(battleStack->isShooter())
 			addStatLabel(EStat::SHOTS, battleStack->shots.total(), battleStack->shots.available());
@@ -563,7 +563,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s
 		addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(shooter), parent->info->stackNode->getDefense(shooter));
 		addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(shooter) * dmgMultiply, parent->info->stackNode->getMaxDamage(shooter) * dmgMultiply);
 		addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), parent->info->stackNode->getMaxHealth());
-		addStatLabel(EStat::SPEED, parent->info->creature->speed(), parent->info->stackNode->speed());
+		addStatLabel(EStat::SPEED, parent->info->creature->getMovementRange(), parent->info->stackNode->getMovementRange());
 
 		if(shooter)
 			addStatLabel(EStat::SHOTS, parent->info->stackNode->valOfBonuses(BonusType::SHOTS));

+ 7 - 0
client/windows/settings/AdventureOptionsTab.cpp

@@ -134,6 +134,10 @@ AdventureOptionsTab::AdventureOptionsTab()
 	{
 		return setBoolSetting("gameTweaks", "skipAdventureMapAnimations", value);
 	});
+	addCallback("hideBackgroundChanged", [](bool value)
+	{
+		return setBoolSetting("adventure", "hideBackground", value);
+	});
 	build(config);
 
 	std::shared_ptr<CToggleGroup> playerHeroSpeedToggle = widget<CToggleGroup>("heroMovementSpeedPicker");
@@ -179,4 +183,7 @@ AdventureOptionsTab::AdventureOptionsTab()
 
 	std::shared_ptr<CToggleButton> skipAdventureMapAnimationsCheckbox = widget<CToggleButton>("skipAdventureMapAnimationsCheckbox");
 	skipAdventureMapAnimationsCheckbox->setSelected(settings["gameTweaks"]["skipAdventureMapAnimations"].Bool());
+
+	std::shared_ptr<CToggleButton> hideBackgroundCheckbox = widget<CToggleButton>("hideBackgroundCheckbox");
+	hideBackgroundCheckbox->setSelected(settings["adventure"]["hideBackground"].Bool());
 }

+ 2 - 0
cmake_modules/VCMI_lib.cmake

@@ -156,6 +156,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/rmg/modificators/ObjectDistributor.cpp
 		${MAIN_LIB_DIR}/rmg/modificators/RoadPlacer.cpp
 		${MAIN_LIB_DIR}/rmg/modificators/TreasurePlacer.cpp
+		${MAIN_LIB_DIR}/rmg/modificators/PrisonHeroPlacer.cpp
 		${MAIN_LIB_DIR}/rmg/modificators/QuestArtifactPlacer.cpp
 		${MAIN_LIB_DIR}/rmg/modificators/ConnectionsPlacer.cpp
 		${MAIN_LIB_DIR}/rmg/modificators/WaterAdopter.cpp
@@ -526,6 +527,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/rmg/modificators/ObjectDistributor.h
 		${MAIN_LIB_DIR}/rmg/modificators/RoadPlacer.h
 		${MAIN_LIB_DIR}/rmg/modificators/TreasurePlacer.h
+		${MAIN_LIB_DIR}/rmg/modificators/PrisonHeroPlacer.h
 		${MAIN_LIB_DIR}/rmg/modificators/QuestArtifactPlacer.h
 		${MAIN_LIB_DIR}/rmg/modificators/ConnectionsPlacer.h
 		${MAIN_LIB_DIR}/rmg/modificators/WaterAdopter.h

+ 0 - 7
config/bonuses.json

@@ -3,13 +3,6 @@
 // LEVEL_SPELL_IMMUNITY
 
 {
-	"ACCURATE_SHOT":
-	{
-		"graphics":
-		{
-			"icon":  "zvs/Lib1.res/E_DIST"
-		}
-	},
 	"ADDITIONAL_ATTACK":
 	{
 		"graphics":

+ 0 - 6
config/creatures/dungeon.json

@@ -411,12 +411,6 @@
 				"type" : "LEVEL_SPELL_IMMUNITY",
 				"val" : 5
 			},
-			"hateGiants" :
-			{
-				"type" : "HATE",
-				"subtype" : "creature.giant",
-				"val" : 50
-			},
 			"hateTitans" :
 			{
 				"type" : "HATE",

+ 2 - 0
config/gameConfig.json

@@ -371,6 +371,8 @@
 		
 		"pathfinder" :
 		{
+			// if enabled, pathfinder will build path through locations guarded by wandering monsters
+			"ignoreGuards" : false,
 			// if enabled, pathfinder will take use of any available boats
 			"useBoat" : true,
 			// if enabled, pathfinder will take use of any bidirectional monoliths 

+ 6 - 1
config/schemas/settings.json

@@ -199,6 +199,7 @@
 				},
 				"driver" : {
 					"type" : "string",
+					"defaultWindows" : "",
 					"default" : "opengl",
 					"description" : "preferred graphics backend driver name for SDL2"
 				},
@@ -240,7 +241,7 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default" : {},
-			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel" ],
+			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground" ],
 			"properties" : {
 				"heroMoveTime" : {
 					"type" : "number",
@@ -293,6 +294,10 @@
 					"type" : "number",
 					"default" : 128
 				},
+				"hideBackground" : {
+					"type" : "boolean",
+					"default" : false
+				}
 			}
 		},
 		"battle" : {

+ 8 - 0
config/widgets/settings/adventureOptionsTab.json

@@ -297,6 +297,9 @@
 				},
 				{
 					"text": "vcmi.adventureOptions.showGrid.hover"
+				},
+				{
+					"text": "vcmi.adventureOptions.hideBackground.hover"
 				}
 			]
 		},
@@ -324,6 +327,11 @@
 					"name": "showGridCheckbox",
 					"help": "vcmi.adventureOptions.showGrid",
 					"callback": "showGridChanged"
+				},
+				{
+					"name": "hideBackgroundCheckbox",
+					"help": "vcmi.adventureOptions.hideBackground",
+					"callback": "hideBackgroundChanged"
 				}
 			]
 		},

+ 1 - 0
config/widgets/turnOptionsTab.json

@@ -317,6 +317,7 @@
 		{
 			"name": "buttonSimturnsAI",
 			"position": {"x": 70, "y": 535},
+			"help" : "vcmi.optionsTab.simturnsAI",
 			"type": "toggleButton",
 			"image": "lobby/checkbox",
 			"callback" : "setSimturnAI"

+ 17 - 15
docs/modders/Bonus/Bonus_Types.md

@@ -587,6 +587,11 @@ Affected unit will attack units on all hexes that surround attacked hex
 
 Affected unit will retaliate before enemy attacks, if able
 
+- subtype: 
+	- damageTypeMelee: only melee attacks affected
+	- damageTypeRanged: only ranged attacks affected. Note that unit also requires ability to retaliate in ranged, such as RANGED_RETALIATION bonus
+	- damageTypeAll: any attacks are affected
+
 ### SHOOTS_ALL_ADJACENT
 
 Affected unit will attack units on all hexes that surround attacked hex in ranged attacks
@@ -727,14 +732,20 @@ Affected unit will deal additional damage after attack
 
 ### DEATH_STARE
 
-Affected unit will kill additional units after attack
+Affected unit will kill additional units after attack. Used for Death stare (Mighty Gorgon) ability and for Accurate Shot (Pirates, HotA)
 
 - subtype: 
-	- deathStareGorgon: random amount
-	- deathStareCommander: fixed amount
+	- deathStareGorgon: only melee attack, random amount of killed units
+	- deathStareNoRangePenalty: only ranged attacks without obstacle (walls) or range penalty
+	- deathStareRangePenalty: only ranged attacks with range penalty
+	- deathStareObstaclePenalty: only ranged attacks with obstacle (walls) penalty
+	- deathStareRangeObstaclePenalty: only ranged attacks with both range and obstacle penalty
+	- deathStareCommander: fixed amount, both melee and ranged attacks
 - val: 
-	- for deathStareGorgon: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once. TODO: recheck formula
 	- for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val
+	- for all other subtypes: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once, rounded up
+- addInfo:
+	- SpellID to be used as hit effect. If not set - 'deathStare' spell will be used. If set to "accurateShot" battle log messages will use alternative description
 
 ### SPECIAL_CRYSTAL_GENERATION
 
@@ -744,15 +755,6 @@ If player has affected unit under his control in any army, he will receive addit
 
 Affected unit will not use spellcast as default attack option
 
-### ACCURATE_SHOT
-
-Affected unit will kill additional units after attack, similar to death stare - works only for ranged attack
-
-- subtype:
-	spell identifier for spell that receives value that should be killed on input, spell.deathStare is used by default, use 'accurateShot' as part of spell name to allow detection for proper battle log description
-- val:
-	chance to kill, counted separately for each unit in attacking stack, percentage. Chance gets lessened by 2/3 with range penalty and effect won't trigger with wall penalty. At most (stack size \* chance / 100 **[rounded up]**) units can be killed at once. TODO: recheck formula
-
 ## Creature spellcasting and activated abilities
 
 ### SPELLCASTER
@@ -788,7 +790,7 @@ Determines how many times per combat affected creature can cast its targeted spe
 - subtype - spell id, eg. spell.iceBolt
 - value - chance (percent)
 - additional info - \[X, Y, Z\]
-    - X - spell level
+    - X - spell mastery level (1 - Basic, 3 - Expert)
     - Y = 0 - all attacks, 1 - shot only, 2 - melee only
     - Z (optional) - layer for multiple SPELL_AFTER_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering.
   When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired.
@@ -798,7 +800,7 @@ Determines how many times per combat affected creature can cast its targeted spe
 - subtype - spell id
 - value - chance %
 - additional info - \[X, Y, Z\]
-    - X - spell level
+    - X - spell mastery level (1 - Basic, 3 - Expert)
     - Y = 0 - all attacks, 1 - shot only, 2 - melee only
     - Z (optional) - layer for multiple SPELL_BEFORE_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering.
   When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired.

+ 2 - 2
docs/modders/Entities_Format/Creature_Format.md

@@ -59,8 +59,8 @@ In order to make functional creature you also need:
 	// Basic growth of this creature in town or in external dwellings
 	"growth" : 0,
 	
-	// Bonus growth of this creature from built horde
-	"hordeGrowth" : 0,
+	// Bonus growth of this creature from built horde, if any
+	"horde" : 0,
 	
 	// Creature stats in battle
 	"attack" : 0,

+ 4 - 0
docs/modders/Entities_Format/Spell_Format.md

@@ -61,6 +61,10 @@
 			"positive": true,
 		},
 		
+		// If true, then creature capable of casting this spell can cast this spell on itself
+		// If false, then creature can only cast this spell on other units
+		"canCastOnSelf" : false,
+		
 		// If true, spell won't be available on a map without water
 		"onlyOnWaterMap" : true,
 

+ 1 - 1
docs/players/Game_Mechanics.md

@@ -185,7 +185,7 @@ Following options can be used to configure simultaneous turns:
 
 While simultaneous turns are active, VCMI tracks contacts for each pair of player separately.
 
-Players are considered to be "in contact" if movement range of their heroes at the start of turn overlaps, or, in other words - if their heroes can meet on this turn if both walk towards each other. When calculating movement range, game uses same rules as standard movement range calculation in vcmi, meaning that game will track movement through monoliths and subterranean gates, but will not account for any removable obstacles, such as wandering monsters or treasures that block path between heroes. At the moment, game will not account for any ways to extend movement range - Dimension Door or Town Portal spells, visiting map objects such as Stables, releasing heroes from prisons, etc.
+Players are considered to be "in contact" if movement range of their heroes at the start of turn overlaps, or, in other words - if their heroes can meet on this turn if both walk towards each other. When calculating movement range, game uses rules similar to standard movement range calculation in vcmi, meaning that game will track movement through monoliths and subterranean gates, but will not account for any removable obstacles, such as pickable treasures that block path between heroes. Any existing wandering monsters that block path between heroes are ignored for range calculation. At the moment, game will not account for any ways to extend movement range - Dimension Door or Town Portal spells, visiting map objects such as Stables, releasing heroes from prisons, etc.
 
 Once detected, contact can never be "lost". If game detected contact between two players, this contact will remain active till the end of the game, even if their heroes move far enough from each other.
 

+ 1 - 1
include/vcmi/Creature.h

@@ -23,7 +23,7 @@ class DLL_LINKAGE ACreature: public AFactionMember
 {
 public:
 	bool isLiving() const; //non-undead, non-non living or alive
-	ui32 speed(int turn = 0, bool useBind = false) const; //get speed (in moving tiles) of creature with all modificators
+	ui32 getMovementRange(int turn = 0) const; //get speed (in moving tiles) of creature with all modificators
 	virtual ui32 getMaxHealth() const; //get max HP of stack with all modifiers
 };
 

+ 1 - 0
include/vcmi/spells/Spell.h

@@ -44,6 +44,7 @@ public:
 	virtual bool isMagical() const = 0; //Should this spell considered as magical effect or as ability (like dendroid's bind)
 
 	virtual bool hasSchool(SpellSchool school) const = 0;
+	virtual bool canCastOnSelf() const = 0;
 	virtual void forEachSchool(const SchoolCallback & cb) const = 0;
 	virtual int32_t getCost(const int32_t skillLevel) const = 0;
 

+ 2 - 0
launcher/settingsView/csettingsview_moc.cpp

@@ -169,6 +169,8 @@ static QStringList getAvailableRenderingDrivers()
 	SDL_Init(SDL_INIT_VIDEO);
 	QStringList result;
 
+	result += QString(); // empty value for autoselection
+
 	int driversCount = SDL_GetNumRenderDrivers();
 
 	for(int it = 0; it < driversCount; it++)

+ 2 - 3
lib/BasicTypes.cpp

@@ -168,15 +168,14 @@ ui32 ACreature::getMaxHealth() const
 	return std::max(1, value); //never 0
 }
 
-ui32 ACreature::speed(int turn, bool useBind) const
+ui32 ACreature::getMovementRange(int turn) const
 {
 	//war machines cannot move
 	if(getBonusBearer()->hasBonus(Selector::type()(BonusType::SIEGE_WEAPON).And(Selector::turns(turn))))
 	{
 		return 0;
 	}
-	//bind effect check - doesn't influence stack initiative
-	if(useBind && getBonusBearer()->hasBonus(Selector::type()(BonusType::BIND_EFFECT).And(Selector::turns(turn))))
+	if(getBonusBearer()->hasBonus(Selector::type()(BonusType::BIND_EFFECT).And(Selector::turns(turn))))
 	{
 		return 0;
 	}

+ 4 - 1
lib/CCreatureHandler.cpp

@@ -397,7 +397,10 @@ void CCreature::serializeJson(JsonSerializeFormat & handler)
 	if(!handler.saving)
 	{
 		if(ammMin > ammMax)
+		{
 			logMod->error("Invalid creature '%s' configuration, advMapAmount.min > advMapAmount.max", identifier);
+			std::swap(ammMin, ammMax);
+		}
 	}
 }
 
@@ -622,7 +625,7 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json
 	}
 	else
 	{
-		logGlobal->error("Mod %s: creature %s has minimal damage (%d) greater than maximal damage (%d)!", scope, identifier, minDamage, maxDamage);
+		logMod->error("Mod %s: creature %s has minimal damage (%d) greater than maximal damage (%d)!", scope, identifier, minDamage, maxDamage);
 		cre->addBonus(maxDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin);
 		cre->addBonus(minDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax);
 	}

+ 25 - 0
lib/CGeneralTextHandler.cpp

@@ -264,11 +264,14 @@ void TextLocalizationContainer::registerStringOverride(const std::string & modCo
 
 void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container)
 {
+	assert(!vstd::contains(subContainers, &container));
 	subContainers.push_back(&container);
 }
 
 void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container)
 {
+	assert(vstd::contains(subContainers, &container));
+
 	subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end());
 }
 
@@ -414,6 +417,28 @@ void TextLocalizationContainer::jsonSerialize(JsonNode & dest) const
 	}
 }
 
+TextContainerRegistrable::TextContainerRegistrable()
+{
+	VLC->generaltexth->addSubContainer(*this);
+}
+
+TextContainerRegistrable::~TextContainerRegistrable()
+{
+	VLC->generaltexth->removeSubContainer(*this);
+}
+
+TextContainerRegistrable::TextContainerRegistrable(const TextContainerRegistrable & other)
+	: TextLocalizationContainer(other)
+{
+	VLC->generaltexth->addSubContainer(*this);
+}
+
+TextContainerRegistrable::TextContainerRegistrable(TextContainerRegistrable && other) noexcept
+	:TextLocalizationContainer(other)
+{
+	VLC->generaltexth->addSubContainer(*this);
+}
+
 void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName)
 {
 	CLegacyConfigParser parser(TextPath::builtin(sourceName));

+ 12 - 0
lib/CGeneralTextHandler.h

@@ -218,6 +218,18 @@ public:
 	}
 };
 
+class DLL_LINKAGE TextContainerRegistrable : public TextLocalizationContainer
+{
+public:
+	TextContainerRegistrable();
+	~TextContainerRegistrable();
+
+	TextContainerRegistrable(const TextContainerRegistrable & other);
+	TextContainerRegistrable(TextContainerRegistrable && other) noexcept;
+
+	TextContainerRegistrable& operator=(TextContainerRegistrable b) = delete;
+};
+
 /// Handles all text-related data in game
 class DLL_LINKAGE CGeneralTextHandler: public TextLocalizationContainer
 {

+ 5 - 1
lib/CHeroHandler.cpp

@@ -473,7 +473,11 @@ void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const
 		hero->initialArmy[i].minAmount = static_cast<ui32>(source["min"].Float());
 		hero->initialArmy[i].maxAmount = static_cast<ui32>(source["max"].Float());
 
-		assert(hero->initialArmy[i].minAmount <= hero->initialArmy[i].maxAmount);
+		if (hero->initialArmy[i].minAmount > hero->initialArmy[i].maxAmount)
+		{
+			logMod->error("Hero %s has minimal army size (%d) greater than maximal size (%d)!", hero->getJsonKey(), hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount);
+			std::swap(hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount);
+		}
 
 		VLC->identifiers()->requestIdentifier("creature", source["creature"], [=](si32 creature)
 		{

+ 8 - 1
lib/CRandomGenerator.cpp

@@ -37,21 +37,25 @@ void CRandomGenerator::resetSeed()
 
 TRandI CRandomGenerator::getIntRange(int lower, int upper)
 {
+	assert(lower <= upper);
 	return std::bind(TIntDist(lower, upper), std::ref(rand));
 }
 
 vstd::TRandI64 CRandomGenerator::getInt64Range(int64_t lower, int64_t upper)
 {
+	assert(lower <= upper);
 	return std::bind(TInt64Dist(lower, upper), std::ref(rand));
 }
 
 int CRandomGenerator::nextInt(int upper)
 {
+	assert(0 <= upper);
 	return getIntRange(0, upper)();
 }
 
 int CRandomGenerator::nextInt(int lower, int upper)
 {
+	assert(lower <= upper);
 	return getIntRange(lower, upper)();
 }
 
@@ -62,16 +66,19 @@ int CRandomGenerator::nextInt()
 
 vstd::TRand CRandomGenerator::getDoubleRange(double lower, double upper)
 {
-    return std::bind(TRealDist(lower, upper), std::ref(rand));
+	assert(lower <= upper);
+	return std::bind(TRealDist(lower, upper), std::ref(rand));
 }
 
 double CRandomGenerator::nextDouble(double upper)
 {
+	assert(0 <= upper);
 	return getDoubleRange(0, upper)();
 }
 
 double CRandomGenerator::nextDouble(double lower, double upper)
 {
+	assert(lower <= upper);
 	return getDoubleRange(lower, upper)();
 }
 

+ 1 - 0
lib/GameSettings.cpp

@@ -95,6 +95,7 @@ void GameSettings::load(const JsonNode & input)
 		{EGameSettings::TEXTS_ROAD,                             "textData",  "road"                       },
 		{EGameSettings::TEXTS_SPELL,                            "textData",  "spell"                      },
 		{EGameSettings::TEXTS_TERRAIN,                          "textData",  "terrain"                    },
+		{EGameSettings::PATHFINDER_IGNORE_GUARDS,               "pathfinder", "ignoreGuards"              },
 		{EGameSettings::PATHFINDER_USE_BOAT,                    "pathfinder", "useBoat"                   },
 		{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY,        "pathfinder", "useMonolithTwoWay"         },
 		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique"   },

+ 1 - 0
lib/GameSettings.h

@@ -60,6 +60,7 @@ enum class EGameSettings
 	MAP_FORMAT_JSON_VCMI,
 	MAP_FORMAT_IN_THE_WAKE_OF_GODS,
 	PATHFINDER_USE_BOAT,
+	PATHFINDER_IGNORE_GUARDS,
 	PATHFINDER_USE_MONOLITH_TWO_WAY,
 	PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE,
 	PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM,

+ 1 - 0
lib/IGameCallback.h

@@ -122,6 +122,7 @@ public:
 	virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0;
 	virtual void giveHeroBonus(GiveBonus * bonus)=0;
 	virtual void setMovePoints(SetMovePoints * smp)=0;
+	virtual void setMovePoints(ObjectInstanceID hid, int val, bool absolute)=0;
 	virtual void setManaPoints(ObjectInstanceID hid, int val)=0;
 	virtual void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) = 0;
 	virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0;

+ 7 - 2
lib/JsonNode.cpp

@@ -515,7 +515,6 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso
 		case BonusType::SPECIFIC_SPELL_POWER:
 		case BonusType::ENCHANTED:
 		case BonusType::MORE_DAMAGE_FROM_SPELL:
-		case BonusType::ACCURATE_SHOT:
 		case BonusType::NOT_ACTIVE:
 		{
 			VLC->identifiers()->requestIdentifier( "spell", node, [&subtype](int32_t identifier)
@@ -538,6 +537,7 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso
 		case BonusType::NEGATE_ALL_NATURAL_IMMUNITIES:
 		case BonusType::CREATURE_DAMAGE:
 		case BonusType::FLYING:
+		case BonusType::FIRST_STRIKE:
 		case BonusType::GENERAL_DAMAGE_REDUCTION:
 		case BonusType::PERCENTAGE_DAMAGE_BOOST:
 		case BonusType::SOUL_STEAL:
@@ -1289,7 +1289,12 @@ static JsonNode getDefaultValue(const JsonNode & schema, std::string fieldName)
 #elif defined(VCMI_ANDROID)
 	if (!fieldProps["defaultAndroid"].isNull())
 		return fieldProps["defaultAndroid"];
-#elif !defined(VCMI_MOBILE)
+#elif defined(VCMI_WINDOWS)
+	if (!fieldProps["defaultWindows"].isNull())
+		return fieldProps["defaultWindows"];
+#endif
+
+#if !defined(VCMI_MOBILE)
 	if (!fieldProps["defaultDesktop"].isNull())
 		return fieldProps["defaultDesktop"];
 #endif

+ 6 - 6
lib/battle/CBattleInfoCallback.cpp

@@ -267,7 +267,7 @@ std::vector<PossiblePlayerBattleAction> CBattleInfoCallback::getClientActionsFor
 		allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK); //all active stacks can attack
 		allowedActionList.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere
 
-		if(stack->canMove() && stack->speed(0, true)) //probably no reason to try move war machines or bound stacks
+		if(stack->canMove() && stack->getMovementRange(0)) //probably no reason to try move war machines or bound stacks
 			allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK);
 
 		const auto * siegedTown = battleGetDefendedTown();
@@ -570,7 +570,7 @@ std::vector<BattleHex> CBattleInfoCallback::battleGetAvailableHexes(const Reacha
 	if(!unit->getPosition().isValid()) //turrets
 		return ret;
 
-	auto unitSpeed = unit->speed(0, true);
+	auto unitSpeed = unit->getMovementRange(0);
 
 	const bool tacticsPhase = battleTacticDist() && battleGetTacticsSide() == unit->unitSide();
 
@@ -741,15 +741,15 @@ DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit *
 {
 	RETURN_IF_NOT_BATTLE({});
 	auto reachability = battleGetDistances(attacker, attacker->getPosition());
-	int movementDistance = reachability[attackerPosition];
-	return battleEstimateDamage(attacker, defender, movementDistance, retaliationDmg);
+	int getMovementRange = reachability[attackerPosition];
+	return battleEstimateDamage(attacker, defender, getMovementRange, retaliationDmg);
 }
 
-DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg) const
+DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, DamageEstimation * retaliationDmg) const
 {
 	RETURN_IF_NOT_BATTLE({});
 	const bool shooting = battleCanShoot(attacker, defender->getPosition());
-	const BattleAttackInfo bai(attacker, defender, movementDistance, shooting);
+	const BattleAttackInfo bai(attacker, defender, getMovementRange, shooting);
 	return battleEstimateDamage(bai, retaliationDmg);
 }
 

+ 1 - 1
lib/battle/CBattleInfoCallback.h

@@ -98,7 +98,7 @@ public:
 	/// returns pair <min dmg, max dmg>
 	DamageEstimation battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg = nullptr) const;
 	DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg = nullptr) const;
-	DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg = nullptr) const;
+	DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, DamageEstimation * retaliationDmg = nullptr) const;
 
 	bool battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const;
 	bool battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const;

+ 4 - 0
lib/bonuses/BonusCustomTypes.cpp

@@ -23,6 +23,10 @@ const BonusCustomSubtype BonusCustomSubtype::heroMovementLand(1);
 const BonusCustomSubtype BonusCustomSubtype::heroMovementSea(0);
 const BonusCustomSubtype BonusCustomSubtype::deathStareGorgon(0);
 const BonusCustomSubtype BonusCustomSubtype::deathStareCommander(1);
+const BonusCustomSubtype BonusCustomSubtype::deathStareNoRangePenalty(2);
+const BonusCustomSubtype BonusCustomSubtype::deathStareRangePenalty(3);
+const BonusCustomSubtype BonusCustomSubtype::deathStareObstaclePenalty(4);
+const BonusCustomSubtype BonusCustomSubtype::deathStareRangeObstaclePenalty(5);
 const BonusCustomSubtype BonusCustomSubtype::rebirthRegular(0);
 const BonusCustomSubtype BonusCustomSubtype::rebirthSpecial(1);
 const BonusCustomSubtype BonusCustomSubtype::visionsMonsters(0);

+ 4 - 0
lib/bonuses/BonusCustomTypes.h

@@ -45,6 +45,10 @@ public:
 
 	static const BonusCustomSubtype deathStareGorgon; // 0
 	static const BonusCustomSubtype deathStareCommander;
+	static const BonusCustomSubtype deathStareNoRangePenalty;
+	static const BonusCustomSubtype deathStareRangePenalty;
+	static const BonusCustomSubtype deathStareObstaclePenalty;
+	static const BonusCustomSubtype deathStareRangeObstaclePenalty;
 
 	static const BonusCustomSubtype rebirthRegular; // 0
 	static const BonusCustomSubtype rebirthSpecial; // 1

+ 0 - 1
lib/bonuses/BonusEnum.h

@@ -174,7 +174,6 @@ class JsonNode;
 	BONUS_NAME(MAX_MORALE) /*cheat bonus*/ \
 	BONUS_NAME(MAX_LUCK) /*cheat bonus*/ \
 	BONUS_NAME(FEROCITY) /*extra attacks, only if at least some creatures killed while attacking target unit, val = amount of additional attacks, additional info = amount of creatures killed to trigger (default 1)*/ \
-	BONUS_NAME(ACCURATE_SHOT) /*HotA Sea Dog-like ability - ranged only, val = full arrow trigger percent, subtype = spell identifier that killed value goes through (death stare by default) - use 'accurateShot' as part of spell name for proper battle log description*/ \
 	BONUS_NAME(ENEMY_ATTACK_REDUCTION) /*in % (value) eg. Nix (HotA)*/ \
 	BONUS_NAME(REVENGE) /*additional damage based on how many units in stack died - formula: sqrt((number of creatures at battle start + 1) * creature health) / (total health now + 1 creature health) - 1) * 100% */ \
 	/* end of list */

+ 7 - 7
lib/campaign/CampaignHandler.cpp

@@ -124,7 +124,7 @@ static std::string convertMapName(std::string input)
 	return input;
 }
 
-std::string CampaignHandler::readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier)
+std::string CampaignHandler::readLocalizedString(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier)
 {
 	TextIdentifier stringID( "campaign", convertMapName(filename), identifier);
 
@@ -133,7 +133,7 @@ std::string CampaignHandler::readLocalizedString(CBinaryReader & reader, std::st
 	if (input.empty())
 		return "";
 
-	VLC->generaltexth->registerString(modName, stringID, input);
+	target.getTexts().registerString(modName, stringID, input);
 	return stringID.get();
 }
 
@@ -383,8 +383,8 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader
 	ret.version = static_cast<CampaignVersion>(reader.readUInt32());
 	ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19]
 	ret.loadLegacyData(campId);
-	ret.name.appendTextID(readLocalizedString(reader, filename, modName, encoding, "name"));
-	ret.description.appendTextID(readLocalizedString(reader, filename, modName, encoding, "description"));
+	ret.name.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "name"));
+	ret.description.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "description"));
 	if (ret.version > CampaignVersion::RoE)
 		ret.difficultyChoosenByPlayer = reader.readInt8();
 	else
@@ -396,7 +396,7 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader
 	ret.encoding = encoding;
 }
 
-CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader, const CampaignHeader & header)
+CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader, CampaignHeader & header)
 {
 	auto prologEpilogReader = [&](const std::string & identifier) -> CampaignScenarioPrologEpilog
 	{
@@ -410,7 +410,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader
 			ret.prologVideo = CampaignHandler::prologVideoName(index);
 			ret.prologMusic = CampaignHandler::prologMusicName(reader.readUInt8());
 			ret.prologVoice = isOriginalCampaign ? CampaignHandler::prologVoiceName(index) : AudioPath();
-			ret.prologText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier));
+			ret.prologText.appendTextID(readLocalizedString(header, reader, header.filename, header.modName, header.encoding, identifier));
 		}
 		return ret;
 	};
@@ -428,7 +428,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader
 	}
 	ret.regionColor = reader.readUInt8();
 	ret.difficulty = reader.readUInt8();
-	ret.regionText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region"));
+	ret.regionText.appendTextID(readLocalizedString(header, reader, header.filename, header.modName, header.encoding, ret.mapName + ".region"));
 	ret.prolog = prologEpilogReader(ret.mapName + ".prolog");
 	ret.epilog = prologEpilogReader(ret.mapName + ".epilog");
 

+ 2 - 2
lib/campaign/CampaignHandler.h

@@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class DLL_LINKAGE CampaignHandler
 {
-	static std::string readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier);
+	static std::string readLocalizedString(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier);
 
 	static void readCampaign(Campaign * target, const std::vector<ui8> & stream, std::string filename, std::string modName, std::string encoding);
 
@@ -27,7 +27,7 @@ class DLL_LINKAGE CampaignHandler
 
 	//parsers for original H3C campaigns
 	static void readHeaderFromMemory(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding);
-	static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, const CampaignHeader & header);
+	static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, CampaignHeader & header);
 	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

+ 5 - 0
lib/campaign/CampaignState.cpp

@@ -169,6 +169,11 @@ const CampaignRegions & CampaignHeader::getRegions() const
 	return campaignRegions;
 }
 
+TextContainerRegistrable & CampaignHeader::getTexts()
+{
+	return textContainer;
+}
+
 bool CampaignState::isConquered(CampaignScenarioID whichScenario) const
 {
 	return vstd::contains(mapsConquered, whichScenario);

+ 9 - 3
lib/campaign/CampaignState.h

@@ -9,9 +9,10 @@
  */
 #pragma once
 
-#include "../lib/GameConstants.h"
-#include "../lib/MetaString.h"
-#include "../lib/filesystem/ResourcePath.h"
+#include "../GameConstants.h"
+#include "../MetaString.h"
+#include "../filesystem/ResourcePath.h"
+#include "../CGeneralTextHandler.h"
 #include "CampaignConstants.h"
 #include "CampaignScenarioPrologEpilog.h"
 
@@ -87,6 +88,8 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable
 
 	void loadLegacyData(ui8 campId);
 
+	TextContainerRegistrable textContainer;
+
 public:
 	bool playerSelectedDifficulty() const;
 	bool formatVCMI() const;
@@ -99,6 +102,7 @@ public:
 	AudioPath getMusic() const;
 
 	const CampaignRegions & getRegions() const;
+	TextContainerRegistrable & getTexts();
 
 	template <typename Handler> void serialize(Handler &h, const int formatVersion)
 	{
@@ -112,6 +116,8 @@ public:
 		h & modName;
 		h & music;
 		h & encoding;
+		if (formatVersion >= 832)
+			h & textContainer;
 	}
 };
 

+ 9 - 5
lib/gameState/CGameStateCampaign.cpp

@@ -210,17 +210,21 @@ void CGameStateCampaign::placeCampaignHeroes()
 	// with the same hero type id
 	std::vector<CGHeroInstance *> removedHeroes;
 
-	std::set<HeroTypeID> heroesToRemove = campaignState->getReservedHeroes();
+	std::set<HeroTypeID> reservedHeroes = campaignState->getReservedHeroes();
+	std::set<HeroTypeID> heroesToRemove;
+
+	for (auto const & heroID : reservedHeroes )
+	{
+		// Do not replace reserved heroes initially, e.g. in 1st campaign scenario in which they appear
+		if (!campaignState->getHeroByType(heroID).isNull())
+			heroesToRemove.insert(heroID);
+	}
 
 	for(auto & campaignHeroReplacement : campaignHeroReplacements)
 		heroesToRemove.insert(campaignHeroReplacement.hero->getHeroType());
 
 	for(auto & heroID : heroesToRemove)
 	{
-		// Do not replace reserved heroes initially, e.g. in 1st campaign scenario in which they appear
-		if (campaignState->getHeroByType(heroID).isNull())
-			continue;
-
 		auto * hero = gameState->getUsedHero(heroID);
 		if(hero)
 		{

+ 7 - 1
lib/gameState/TavernHeroesPool.cpp

@@ -40,7 +40,7 @@ TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const
 	return TavernSlotRole::NONE;
 }
 
-void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role)
+void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role, bool replenishPoints)
 {
 	vstd::erase_if(currentTavern, [&](const TavernSlot & entry){
 		return entry.player == player && entry.slot == slot;
@@ -54,6 +54,12 @@ void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot,
 	if (h && army)
 		h->setToArmy(army);
 
+	if (h && replenishPoints)
+	{
+		h->setMovementPoints(h->movementPointsLimit(true));
+		h->mana = h->manaLimit();
+	}
+
 	TavernSlot newSlot;
 	newSlot.hero = h;
 	newSlot.player = player;

+ 1 - 1
lib/gameState/TavernHeroesPool.h

@@ -74,7 +74,7 @@ public:
 	void setAvailability(HeroTypeID hero, std::set<PlayerColor> mask);
 
 	/// Makes hero available in tavern of specified player
-	void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role);
+	void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role, bool replenishPoints);
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{

+ 4 - 4
lib/mapObjects/CGHeroInstance.cpp

@@ -436,14 +436,14 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst)
 
 		int count = rand.nextInt(stack.minAmount, stack.maxAmount);
 
-		const CCreature * creature = stack.creature.toCreature();
-
-		if(creature == nullptr)
+		if(stack.creature == CreatureID::NONE)
 		{
-			logGlobal->error("Hero %s has invalid creature with id %d in initial army", getNameTranslated(), stack.creature.toEnum());
+			logGlobal->error("Hero %s has invalid creature in initial army", getNameTranslated());
 			continue;
 		}
 
+		const CCreature * creature = stack.creature.toCreature();
+
 		if(creature->warMachine != ArtifactID::NONE) //war machine
 		{
 			warMachinesGiven++;

+ 1 - 5
lib/mapObjects/CGTownBuilding.cpp

@@ -151,11 +151,7 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const
 				gb.id = heroID;
 				cb->giveHeroBonus(&gb);
 
-				SetMovePoints mp;
-				mp.val = 600;
-				mp.absolute = false;
-				mp.hid = heroID;
-				cb->setMovePoints(&mp);
+				cb->setMovePoints(heroID, 600, false);
 
 				iw.text.appendRawString(VLC->generaltexth->allTexts[580]);
 				cb->showInfoDialog(&iw);

+ 9 - 0
lib/mapping/CMap.cpp

@@ -649,9 +649,18 @@ void CMap::banWaterHeroes()
 
 void CMap::banHero(const HeroTypeID & id)
 {
+	if (!vstd::contains(allowedHeroes, id))
+		logGlobal->warn("Attempt to ban hero %s, who is already not allowed", id.encode(id));
 	allowedHeroes.erase(id);
 }
 
+void CMap::unbanHero(const HeroTypeID & id)
+{
+	if (vstd::contains(allowedHeroes, id))
+		logGlobal->warn("Attempt to unban hero %s, who is already allowed", id.encode(id));
+	allowedHeroes.insert(id);
+}
+
 void CMap::initTerrain()
 {
 	terrain.resize(boost::extents[levels()][width][height]);

+ 1 - 0
lib/mapping/CMap.h

@@ -112,6 +112,7 @@ public:
 	void banWaterArtifacts();
 	void banWaterHeroes();
 	void banHero(const HeroTypeID& id);
+	void unbanHero(const HeroTypeID & id);
 	void banWaterSpells();
 	void banWaterSkills();
 	void banWaterContent();

+ 3 - 10
lib/mapping/CMapHeader.cpp

@@ -122,13 +122,9 @@ CMapHeader::CMapHeader() : version(EMapFormat::VCMI), height(72), width(72),
 	setupEvents();
 	allowedHeroes = VLC->heroh->getDefaultAllowed();
 	players.resize(PlayerColor::PLAYER_LIMIT_I);
-	VLC->generaltexth->addSubContainer(*this);
 }
 
-CMapHeader::~CMapHeader()
-{
-	VLC->generaltexth->removeSubContainer(*this);
-}
+CMapHeader::~CMapHeader() = default;
 
 ui8 CMapHeader::levels() const
 {
@@ -137,9 +133,6 @@ ui8 CMapHeader::levels() const
 
 void CMapHeader::registerMapStrings()
 {
-	VLC->generaltexth->removeSubContainer(*this);
-	VLC->generaltexth->addSubContainer(*this);
-	
 	//get supported languages. Assuming that translation containing most strings is the base language
 	std::set<std::string> mapLanguages, mapBaseLanguages;
 	int maxStrings = 0;
@@ -193,7 +186,7 @@ void CMapHeader::registerMapStrings()
 		JsonUtils::mergeCopy(data, translations[language]);
 	
 	for(auto & s : data.Struct())
-		registerString("map", TextIdentifier(s.first), s.second.String(), language);
+		texts.registerString("map", TextIdentifier(s.first), s.second.String(), language);
 }
 
 std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized)
@@ -203,7 +196,7 @@ std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeade
 
 std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language)
 {
-	mapHeader.registerString(modContext, UID, localized, language);
+	mapHeader.texts.registerString(modContext, UID, localized, language);
 	mapHeader.translations.Struct()[language].Struct()[UID.get()].String() = localized;
 	return UID.get();
 }

+ 3 - 2
lib/mapping/CMapHeader.h

@@ -192,7 +192,7 @@ struct DLL_LINKAGE TriggeredEvent
 };
 
 /// The map header holds information about loss/victory condition,map format, version, players, height, width,...
-class DLL_LINKAGE CMapHeader: public TextLocalizationContainer
+class DLL_LINKAGE CMapHeader
 {
 	void setupEvents();
 public:
@@ -240,13 +240,14 @@ public:
 	
 	/// translations for map to be transferred over network
 	JsonNode translations;
+	TextContainerRegistrable texts;
 	
 	void registerMapStrings();
 
 	template <typename Handler>
 	void serialize(Handler & h, const int Version)
 	{
-		h & static_cast<TextLocalizationContainer&>(*this);
+		h & texts;
 		h & version;
 		h & mods;
 		h & name;

+ 4 - 0
lib/modding/IdentifierStorage.cpp

@@ -53,6 +53,10 @@ CIdentifierStorage::CIdentifierStorage()
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementSea", 0);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareGorgon", 0);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareCommander", 1);
+	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareNoRangePenalty", 2);
+	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareRangePenalty", 3);
+	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareObstaclePenalty", 4);
+	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareRangeObstaclePenalty", 5);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthRegular", 0);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthSpecial", 1);
 	registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "visionsMonsters", 0);

+ 1 - 1
lib/networkPacks/NetPacksLib.cpp

@@ -959,7 +959,7 @@ void FoWChange::applyGs(CGameState *gs)
 
 void SetAvailableHero::applyGs(CGameState *gs)
 {
-	gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID);
+	gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID, replenishPoints);
 }
 
 void GiveBonus::applyGs(CGameState *gs)

+ 2 - 0
lib/networkPacks/PacksForClient.h

@@ -352,6 +352,7 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient
 	PlayerColor player;
 	HeroTypeID hid; //HeroTypeID::NONE if no hero
 	CSimpleArmy army;
+	bool replenishPoints;
 
 	void visitTyped(ICPackVisitor & visitor) override;
 
@@ -362,6 +363,7 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient
 		h & player;
 		h & hid;
 		h & army;
+		h & replenishPoints;
 	}
 };
 

+ 1 - 0
lib/pathfinder/PathfinderOptions.cpp

@@ -21,6 +21,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 PathfinderOptions::PathfinderOptions()
 	: useFlying(true)
 	, useWaterWalking(true)
+	, ignoreGuards(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_IGNORE_GUARDS))
 	, useEmbarkAndDisembark(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_BOAT))
 	, useTeleportTwoWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY))
 	, useTeleportOneWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE))

+ 1 - 0
lib/pathfinder/PathfinderOptions.h

@@ -25,6 +25,7 @@ struct DLL_LINKAGE PathfinderOptions
 	bool useFlying;
 	bool useWaterWalking;
 	bool useEmbarkAndDisembark;
+	bool ignoreGuards;
 	bool useTeleportTwoWay; // Two-way monoliths and Subterranean Gate
 	bool useTeleportOneWay; // One-way monoliths with one known exit only
 	bool useTeleportOneWayRandom; // One-way monoliths with more than one known exit

+ 7 - 1
lib/pathfinder/PathfindingRules.cpp

@@ -271,7 +271,12 @@ PathfinderBlockingRule::BlockingReason MovementAfterDestinationRule::getBlocking
 	case EPathNodeAction::BATTLE:
 		/// Movement after BATTLE action only possible from guarded tile to guardian tile
 		if(destination.guarded)
-			return BlockingReason::DESTINATION_GUARDED;
+		{
+			if (pathfinderHelper->options.ignoreGuards)
+				return BlockingReason::DESTINATION_GUARDED;
+			else
+				return BlockingReason::NONE;
+		}
 
 		break;
 	}
@@ -299,6 +304,7 @@ PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingRea
 		if(source.guarded)
 		{
 			if(!(pathfinderConfig->options.originalMovementRules && source.node->layer == EPathfindingLayer::AIR) 
+				&& !pathfinderConfig->options.ignoreGuards
 				&&	(!destination.isGuardianTile || pathfinderHelper->getGuardiansCount(source.coord) > 1)) // Can step into tile of guard
 			{
 				return BlockingReason::SOURCE_GUARDED;

+ 19 - 1
lib/rmg/CMapGenOptions.cpp

@@ -388,6 +388,13 @@ void CMapGenOptions::setStartingTownForPlayer(const PlayerColor & color, Faction
 	it->second.setStartingTown(town);
 }
 
+void CMapGenOptions::setStartingHeroForPlayer(const PlayerColor & color, HeroTypeID hero)
+{
+	auto it = players.find(color);
+	assert(it != players.end());
+	it->second.setStartingHero(hero);
+}
+
 void CMapGenOptions::setPlayerTypeForStandardPlayer(const PlayerColor & color, EPlayerType playerType)
 {
 	// FIXME: Why actually not set it to COMP_ONLY? Ie. when swapping human to another color?
@@ -746,7 +753,7 @@ const CRmgTemplate * CMapGenOptions::getPossibleTemplate(CRandomGenerator & rand
 	return *RandomGeneratorUtil::nextItem(templates, rand);
 }
 
-CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(FactionID::RANDOM), playerType(EPlayerType::AI), team(TeamID::NO_TEAM)
+CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(FactionID::RANDOM), startingHero(HeroTypeID::RANDOM), playerType(EPlayerType::AI), team(TeamID::NO_TEAM)
 {
 
 }
@@ -778,6 +785,17 @@ void CMapGenOptions::CPlayerSettings::setStartingTown(FactionID value)
 	startingTown = value;
 }
 
+HeroTypeID CMapGenOptions::CPlayerSettings::getStartingHero() const
+{
+	return startingHero;
+}
+
+void CMapGenOptions::CPlayerSettings::setStartingHero(HeroTypeID value)
+{
+	assert(value == HeroTypeID::RANDOM || value.toEntity(VLC) != nullptr);
+	startingHero = value;
+}
+
 EPlayerType CMapGenOptions::CPlayerSettings::getPlayerType() const
 {
 	return playerType;

+ 11 - 0
lib/rmg/CMapGenOptions.h

@@ -45,6 +45,11 @@ public:
 		FactionID getStartingTown() const;
 		void setStartingTown(FactionID value);
 
+		/// The starting hero of the player ranging from 0 to hero max count or RANDOM_HERO.
+		/// The default value is RANDOM_HERO
+		HeroTypeID getStartingHero() const;
+		void setStartingHero(HeroTypeID value);
+
 		/// The default value is EPlayerType::AI.
 		EPlayerType getPlayerType() const;
 		void setPlayerType(EPlayerType value);
@@ -56,6 +61,7 @@ public:
 	private:
 		PlayerColor color;
 		FactionID startingTown;
+		HeroTypeID startingHero;
 		EPlayerType playerType;
 		TeamID team;
 
@@ -68,6 +74,10 @@ public:
 			h & playerType;
 			if(version >= 806)
 				h & team;
+			if (version >= 832)
+				h & startingHero;
+			else
+				startingHero = HeroTypeID::RANDOM;
 		}
 	};
 
@@ -120,6 +130,7 @@ public:
 	const std::map<PlayerColor, CPlayerSettings> & getPlayersSettings() const;
 	const std::map<PlayerColor, CPlayerSettings> & getSavedPlayersMap() const;
 	void setStartingTownForPlayer(const PlayerColor & color, FactionID town);
+	void setStartingHeroForPlayer(const PlayerColor & color, HeroTypeID hero);
 	/// Sets a player type for a standard player. A standard player is the opposite of a computer only player. The
 	/// values which can be chosen for the player type are EPlayerType::AI or EPlayerType::HUMAN.
 	void setPlayerTypeForStandardPlayer(const PlayerColor & color, EPlayerType playerType);

+ 21 - 22
lib/rmg/CMapGenerator.cpp

@@ -35,7 +35,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 CMapGenerator::CMapGenerator(CMapGenOptions& mapGenOptions, int RandomSeed) :
 	mapGenOptions(mapGenOptions), randomSeed(RandomSeed),
-	allowedPrisons(0), monolithIndex(0)
+	monolithIndex(0)
 {
 	loadConfig();
 	rand.setSeed(this->randomSeed);
@@ -96,12 +96,6 @@ const CMapGenOptions& CMapGenerator::getMapGenOptions() const
 	return mapGenOptions;
 }
 
-void CMapGenerator::initPrisonsRemaining()
-{
-	allowedPrisons = map->getMap(this).allowedHeroes.size();
-	allowedPrisons = std::max<int> (0, allowedPrisons - 16 * mapGenOptions.getHumanOrCpuPlayerCount()); //so at least 16 heroes will be available for every player
-}
-
 void CMapGenerator::initQuestArtsRemaining()
 {
 	//TODO: Move to QuestArtifactPlacer?
@@ -122,7 +116,6 @@ std::unique_ptr<CMap> CMapGenerator::generate()
 		addHeaderInfo();
 		map->initTiles(*this, rand);
 		Load::Progress::step();
-		initPrisonsRemaining();
 		initQuestArtsRemaining();
 		genZones();
 		Load::Progress::step();
@@ -468,11 +461,6 @@ int CMapGenerator::getNextMonlithIndex()
 	}
 }
 
-int CMapGenerator::getPrisonsRemaning() const
-{
-	return allowedPrisons;
-}
-
 std::shared_ptr<CZonePlacer> CMapGenerator::getZonePlacer() const
 {
 	return placer;
@@ -488,31 +476,42 @@ const std::vector<HeroTypeID> CMapGenerator::getAllPossibleHeroes() const
 	auto isWaterMap = map->getMap(this).isWaterMap();
 	//Skip heroes that were banned, including the ones placed in prisons
 	std::vector<HeroTypeID> ret;
+
 	for (HeroTypeID hero : map->getMap(this).allowedHeroes)
 	{
 		auto * h = dynamic_cast<const CHero*>(VLC->heroTypes()->getById(hero));
-		if ((h->onlyOnWaterMap && !isWaterMap) || (h->onlyOnMapWithoutWater && isWaterMap))
-		{
+		if(h->onlyOnWaterMap && !isWaterMap)
 			continue;
-		}
-		else
+
+		if(h->onlyOnMapWithoutWater && isWaterMap)
+			continue;
+
+		bool heroUsedAsStarting = false;
+		for (auto const & player : map->getMapGenOptions().getPlayersSettings())
 		{
-			ret.push_back(hero);
+			if (player.second.getStartingHero() == hero)
+			{
+				heroUsedAsStarting = true;
+				break;
+			}
 		}
+
+		if (heroUsedAsStarting)
+			continue;
+
+		ret.push_back(hero);
 	}
 	return ret;
 }
 
 void CMapGenerator::banQuestArt(const ArtifactID & id)
 {
-	//TODO: Protect with mutex
 	map->getMap(this).allowedArtifact.erase(id);
 }
 
-void CMapGenerator::banHero(const HeroTypeID & id)
+void CMapGenerator::unbanQuestArt(const ArtifactID & id)
 {
-	//TODO: Protect with mutex
-	map->getMap(this).banHero(id);
+	map->getMap(this).allowedArtifact.insert(id);
 }
 
 Zone * CMapGenerator::getZoneWater() const

+ 1 - 3
lib/rmg/CMapGenerator.h

@@ -65,8 +65,7 @@ public:
 	const std::vector<ArtifactID> & getAllPossibleQuestArtifacts() const;
 	const std::vector<HeroTypeID> getAllPossibleHeroes() const;
 	void banQuestArt(const ArtifactID & id);
-	void banHero(const HeroTypeID& id);
-
+	void unbanQuestArt(const ArtifactID & id);
 	Zone * getZoneWater() const;
 	void addWaterTreasuresInfo();
 
@@ -82,7 +81,6 @@ private:
 	
 	std::vector<rmg::ZoneConnection> connectionsLeft;
 	
-	int allowedPrisons;
 	int monolithIndex;
 	std::vector<ArtifactID> questArtifacts;
 

+ 7 - 0
lib/rmg/RmgMap.cpp

@@ -19,6 +19,7 @@
 #include "modificators/ObjectManager.h"
 #include "modificators/RoadPlacer.h"
 #include "modificators/TreasurePlacer.h"
+#include "modificators/PrisonHeroPlacer.h"
 #include "modificators/QuestArtifactPlacer.h"
 #include "modificators/ConnectionsPlacer.h"
 #include "modificators/TownPlacer.h"
@@ -127,6 +128,7 @@ void RmgMap::initTiles(CMapGenerator & generator, CRandomGenerator & rand)
 void RmgMap::addModificators()
 {
 	bool hasObjectDistributor = false;
+	bool hasHeroPlacer = false;
 	bool hasRockFiller = false;
 
 	for(auto & z : getZones())
@@ -139,6 +141,11 @@ void RmgMap::addModificators()
 			zone->addModificator<ObjectDistributor>();
 			hasObjectDistributor = true;
 		}
+		if (!hasHeroPlacer)
+		{
+			zone->addModificator<PrisonHeroPlacer>();
+			hasHeroPlacer = true;
+		}
 		zone->addModificator<TreasurePlacer>();
 		zone->addModificator<ObstaclePlacer>();
 		zone->addModificator<TerrainPainter>();

+ 13 - 2
lib/rmg/modificators/ObjectDistributor.cpp

@@ -15,6 +15,7 @@
 #include "../RmgMap.h"
 #include "../CMapGenerator.h"
 #include "TreasurePlacer.h"
+#include "PrisonHeroPlacer.h"
 #include "QuestArtifactPlacer.h"
 #include "TownPlacer.h"
 #include "TerrainPainter.h"
@@ -75,7 +76,6 @@ void ObjectDistributor::distributeLimitedObjects()
 
 					auto rmgInfo = handler->getRMGInfo();
 
-					// FIXME: Random order of distribution
 					RandomGeneratorUtil::randomShuffle(matchingZones, zone.getRand());
 					for (auto& zone : matchingZones)
 					{
@@ -146,7 +146,18 @@ void ObjectDistributor::distributePrisons()
 
 	RandomGeneratorUtil::randomShuffle(zones, zone.getRand());
 
-	size_t allowedPrisons = generator.getPrisonsRemaning();
+	// TODO: Some shorthand for unique Modificator
+	PrisonHeroPlacer * prisonHeroPlacer = nullptr;
+	for(auto & z : map.getZones())
+	{
+		prisonHeroPlacer = z.second->getModificator<PrisonHeroPlacer>();
+		if (prisonHeroPlacer)
+		{
+			break;
+		}
+	}
+
+	size_t allowedPrisons = prisonHeroPlacer->getPrisonsRemaning();
 	for (int i = zones.size() - 1; i >= 0; i--)
 	{
 		auto zone = zones[i].second;

+ 73 - 0
lib/rmg/modificators/PrisonHeroPlacer.cpp

@@ -0,0 +1,73 @@
+/*
+* PrisonHeroPlacer.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+
+#include "StdInc.h"
+#include "PrisonHeroPlacer.h"
+#include "../CMapGenerator.h"
+#include "../RmgMap.h"
+#include "TreasurePlacer.h"
+#include "../CZonePlacer.h"
+#include "../../VCMI_Lib.h"
+#include "../../mapObjectConstructors/AObjectTypeHandler.h"
+#include "../../mapObjectConstructors/CObjectClassesHandler.h"
+#include "../../mapObjects/MapObjects.h" 
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+void PrisonHeroPlacer::process()
+{
+	getAllowedHeroes();
+}
+
+void PrisonHeroPlacer::init()
+{
+	// Reserve at least 16 heroes for each player
+	reservedHeroes = 16 * generator.getMapGenOptions().getHumanOrCpuPlayerCount();
+}
+
+void PrisonHeroPlacer::getAllowedHeroes()
+{
+	// TODO: Give each zone unique HeroPlacer with private hero list?
+
+	// Call that only once
+	if (allowedHeroes.empty())
+	{
+    	allowedHeroes = generator.getAllPossibleHeroes();
+	}
+}
+
+int PrisonHeroPlacer::getPrisonsRemaning() const
+{
+	return std::max<int>(allowedHeroes.size() - reservedHeroes, 0);
+}
+
+HeroTypeID PrisonHeroPlacer::drawRandomHero()
+{
+	RecursiveLock lock(externalAccessMutex);
+	if (getPrisonsRemaning() > 0)
+	{
+		RandomGeneratorUtil::randomShuffle(allowedHeroes, zone.getRand());
+        HeroTypeID ret = allowedHeroes.back();
+        allowedHeroes.pop_back();
+		return ret;
+	}
+	else
+	{
+		throw rmgException("No unused heroes left for prisons!");
+	}
+}
+
+void PrisonHeroPlacer::restoreDrawnHero(const HeroTypeID & hid)
+{
+	RecursiveLock lock(externalAccessMutex);
+	allowedHeroes.push_back(hid);
+}
+
+VCMI_LIB_NAMESPACE_END

+ 41 - 0
lib/rmg/modificators/PrisonHeroPlacer.h

@@ -0,0 +1,41 @@
+/*
+* PrisonHeroPlacer, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+
+#pragma once
+#include "../Zone.h"
+#include "../Functions.h"
+#include "../../mapObjects/ObjectTemplate.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CRandomGenerator;
+
+class PrisonHeroPlacer : public Modificator
+{
+public:
+	MODIFICATOR(PrisonHeroPlacer);
+
+	void process() override;
+	void init() override;
+
+	int getPrisonsRemaning() const;
+	[[nodiscard]] HeroTypeID drawRandomHero();
+	void restoreDrawnHero(const HeroTypeID & hid);
+
+private:
+    void getAllowedHeroes();
+	size_t reservedHeroes;
+
+protected:
+
+    std::vector<HeroTypeID> allowedHeroes;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 12 - 3
lib/rmg/modificators/QuestArtifactPlacer.cpp

@@ -40,11 +40,18 @@ void QuestArtifactPlacer::addQuestArtZone(std::shared_ptr<Zone> otherZone)
 
 void QuestArtifactPlacer::addQuestArtifact(const ArtifactID& id)
 {
+	logGlobal->info("Need to place quest artifact %s", VLC->artifacts()->getById(id)->getNameTranslated());
 	RecursiveLock lock(externalAccessMutex);
-	logGlobal->info("Need to place quest artifact artifact %s", VLC->artifacts()->getById(id)->getNameTranslated());
 	questArtifactsToPlace.emplace_back(id);
 }
 
+void QuestArtifactPlacer::removeQuestArtifact(const ArtifactID& id)
+{
+	logGlobal->info("Will not try to place quest artifact %s", VLC->artifacts()->getById(id)->getNameTranslated());
+	RecursiveLock lock(externalAccessMutex);
+	vstd::erase_if_present(questArtifactsToPlace, id);
+}
+
 void QuestArtifactPlacer::rememberPotentialArtifactToReplace(CGObjectInstance* obj)
 {
 	RecursiveLock lock(externalAccessMutex);
@@ -131,9 +138,10 @@ ArtifactID QuestArtifactPlacer::drawRandomArtifact()
 	RecursiveLock lock(externalAccessMutex);
 	if (!questArtifacts.empty())
 	{
+		RandomGeneratorUtil::randomShuffle(questArtifacts, zone.getRand());
 		ArtifactID ret = questArtifacts.back();
 		questArtifacts.pop_back();
-		RandomGeneratorUtil::randomShuffle(questArtifacts, zone.getRand());
+		generator.banQuestArt(ret);
 		return ret;
 	}
 	else
@@ -142,10 +150,11 @@ ArtifactID QuestArtifactPlacer::drawRandomArtifact()
 	}
 }
 
-void QuestArtifactPlacer::addRandomArtifact(ArtifactID artid)
+void QuestArtifactPlacer::addRandomArtifact(const ArtifactID & artid)
 {
 	RecursiveLock lock(externalAccessMutex);
 	questArtifacts.push_back(artid);
+	generator.unbanQuestArt(artid);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 3 - 2
lib/rmg/modificators/QuestArtifactPlacer.h

@@ -29,14 +29,15 @@ public:
 	void findZonesForQuestArts();
 
 	void addQuestArtifact(const ArtifactID& id);
+	void removeQuestArtifact(const ArtifactID& id);
 	void rememberPotentialArtifactToReplace(CGObjectInstance* obj);
 	std::vector<CGObjectInstance*> getPossibleArtifactsToReplace() const;
 	void placeQuestArtifacts(CRandomGenerator & rand);
 	void dropReplacedArtifact(CGObjectInstance* obj);
 
 	size_t getMaxQuestArtifactCount() const;
-	ArtifactID drawRandomArtifact();
-	void addRandomArtifact(ArtifactID artid);
+	[[nodiscard]] ArtifactID drawRandomArtifact();
+	void addRandomArtifact(const ArtifactID & artid);
 
 protected:
 

+ 57 - 26
lib/rmg/modificators/TreasurePlacer.cpp

@@ -18,6 +18,7 @@
 #include "../RmgMap.h"
 #include "../TileInfo.h"
 #include "../CZonePlacer.h"
+#include "PrisonHeroPlacer.h"
 #include "QuestArtifactPlacer.h"
 #include "../../ArtifactUtils.h"
 #include "../../mapObjectConstructors/AObjectTypeHandler.h"
@@ -32,6 +33,12 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+ObjectInfo::ObjectInfo():
+	destroyObject([](){})
+{
+
+}
+
 void TreasurePlacer::process()
 {
 	addAllPossibleObjects();
@@ -45,6 +52,7 @@ void TreasurePlacer::init()
 	maxPrisons = 0; //Should be in the constructor, but we use macro for that
 	DEPENDENCY(ObjectManager);
 	DEPENDENCY(ConnectionsPlacer);
+	DEPENDENCY_ALL(PrisonHeroPlacer);
 	POSTFUNCTION(RoadPlacer);
 }
 
@@ -90,6 +98,16 @@ void TreasurePlacer::addAllPossibleObjects()
 	auto prisonTemplates = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0)->getTemplates(zone.getTerrainType());
 	if (!prisonTemplates.empty())
 	{
+		PrisonHeroPlacer * prisonHeroPlacer = nullptr;
+		for(auto & z : map.getZones())
+		{
+			prisonHeroPlacer = z.second->getModificator<PrisonHeroPlacer>();
+		 	if (prisonHeroPlacer)
+			{
+				break;
+			}
+		}
+
 		//prisons
 		//levels 1, 5, 10, 20, 30
 		static int prisonsLevels = std::min(generator.getConfig().prisonExperience.size(), generator.getConfig().prisonValues.size());
@@ -97,16 +115,22 @@ void TreasurePlacer::addAllPossibleObjects()
 		size_t prisonsLeft = getMaxPrisons();
 		for (int i = prisonsLevels - 1; i >= 0; i--)
 		{
+			ObjectInfo oi; // Create new instance which will hold destructor operation
+
 			oi.value = generator.getConfig().prisonValues[i];
 			if (oi.value > zone.getMaxTreasureValue())
 			{
 				continue;
 			}
 
-			oi.generateObject = [i, this]() -> CGObjectInstance*
+			oi.generateObject = [i, this, prisonHeroPlacer, &oi]() -> CGObjectInstance*
 			{
-				auto possibleHeroes = generator.getAllPossibleHeroes();
-				HeroTypeID hid = *RandomGeneratorUtil::nextItem(possibleHeroes, zone.getRand());
+				HeroTypeID hid = prisonHeroPlacer->drawRandomHero();
+				oi.destroyObject = [hid, prisonHeroPlacer]()
+				{
+					// Hero can be used again
+					prisonHeroPlacer->restoreDrawnHero(hid);
+				};
 
 				auto factory = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0);
 				auto* obj = dynamic_cast<CGHeroInstance*>(factory->create());
@@ -114,7 +138,6 @@ void TreasurePlacer::addAllPossibleObjects()
 				obj->setHeroType(hid); //will be initialized later
 				obj->exp = generator.getConfig().prisonExperience[i];
 				obj->setOwner(PlayerColor::NEUTRAL);
-				generator.banHero(hid);
 
 				return obj;
 			};
@@ -441,6 +464,19 @@ void TreasurePlacer::addAllPossibleObjects()
 		
 		RandomGeneratorUtil::randomShuffle(creatures, zone.getRand());
 
+		auto setRandomArtifact = [qap, &oi](CGSeerHut * obj)
+		{
+			ArtifactID artid = qap->drawRandomArtifact();
+			oi.destroyObject = [artid, qap]()
+			{
+				// Artifact can be used again
+				qap->addRandomArtifact(artid);
+				qap->removeQuestArtifact(artid);
+			};
+			obj->quest->mission.artifacts.push_back(artid);
+			qap->addQuestArtifact(artid);
+		};
+
 		for(int i = 0; i < static_cast<int>(creatures.size()); i++)
 		{
 			auto * creature = creatures[i];
@@ -451,7 +487,8 @@ void TreasurePlacer::addAllPossibleObjects()
 			
 			int randomAppearance = chooseRandomAppearance(zone.getRand(), Obj::SEER_HUT, zone.getTerrainType());
 			
-			oi.generateObject = [creature, creaturesAmount, randomAppearance, this, qap]() -> CGObjectInstance *
+			// FIXME: Remove duplicated code for gold, exp and creaure reward
+			oi.generateObject = [creature, creaturesAmount, randomAppearance, setRandomArtifact]() -> CGObjectInstance *
 			{
 				auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance);
 				auto * obj = dynamic_cast<CGSeerHut *>(factory->create());
@@ -461,11 +498,7 @@ void TreasurePlacer::addAllPossibleObjects()
 				reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT;
 				obj->configuration.info.push_back(reward);
 								
-				ArtifactID artid = qap->drawRandomArtifact();
-				obj->quest->mission.artifacts.push_back(artid);
-				
-				generator.banQuestArt(artid);
-				zone.getModificator<QuestArtifactPlacer>()->addQuestArtifact(artid);
+				setRandomArtifact(obj);
 				
 				return obj;
 			};
@@ -499,7 +532,7 @@ void TreasurePlacer::addAllPossibleObjects()
 			oi.probability = 10;
 			oi.maxPerZone = 1;
 			
-			oi.generateObject = [i, randomAppearance, this, qap]() -> CGObjectInstance *
+			oi.generateObject = [i, randomAppearance, this, setRandomArtifact]() -> CGObjectInstance *
 			{
 				auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance);
 				auto * obj = dynamic_cast<CGSeerHut *>(factory->create());
@@ -508,20 +541,16 @@ void TreasurePlacer::addAllPossibleObjects()
 				reward.reward.heroExperience = generator.getConfig().questRewardValues[i];
 				reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT;
 				obj->configuration.info.push_back(reward);
-				
-				ArtifactID artid = qap->drawRandomArtifact();
-				obj->quest->mission.artifacts.push_back(artid);
-				
-				generator.banQuestArt(artid);
-				zone.getModificator<QuestArtifactPlacer>()->addQuestArtifact(artid);
-				
+
+				setRandomArtifact(obj);
+
 				return obj;
 			};
 			
 			if(!oi.templates.empty())
 				possibleSeerHuts.push_back(oi);
 			
-			oi.generateObject = [i, randomAppearance, this, qap]() -> CGObjectInstance *
+			oi.generateObject = [i, randomAppearance, this, setRandomArtifact]() -> CGObjectInstance *
 			{
 				auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance);
 				auto * obj = dynamic_cast<CGSeerHut *>(factory->create());
@@ -531,11 +560,7 @@ void TreasurePlacer::addAllPossibleObjects()
 				reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT;
 				obj->configuration.info.push_back(reward);
 				
-				ArtifactID artid = qap->drawRandomArtifact();
-				obj->quest->mission.artifacts.push_back(artid);
-				
-				generator.banQuestArt(artid);
-				zone.getModificator<QuestArtifactPlacer>()->addQuestArtifact(artid);
+				setRandomArtifact(obj);
 				
 				return obj;
 			};
@@ -641,8 +666,14 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 		}
 		
 		auto * object = oi->generateObject();
+
 		if(oi->templates.empty())
+		{
+			logGlobal->warn("Deleting randomized object with no templates: %s", object->getObjectName());
+			oi->destroyObject();
+			delete object;
 			continue;
+		}
 		
 		auto templates = object->getObjectHandler()->getMostSpecificTemplates(zone.getTerrainType());
 
@@ -721,7 +752,7 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 					instanceAccessibleArea.add(instance.getVisitablePosition());
 			}
 			
-			//first object is good
+			//Do not clean up after first object
 			if(rmgObject.instances().size() == 1)
 				break;
 
@@ -800,10 +831,10 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 	{
 		for (auto* oi : treasurePile)
 		{
+			oi->destroyObject();
 			oi->maxPerZone++;
 		}
 	};
-
 	//place biggest treasures first at large distance, place smaller ones inbetween
 	auto treasureInfo = zone.getTreasureInfo();
 	boost::sort(treasureInfo, valueComparator);

+ 3 - 0
lib/rmg/modificators/TreasurePlacer.h

@@ -22,12 +22,15 @@ class CRandomGenerator;
 
 struct ObjectInfo
 {
+	ObjectInfo();
+
 	std::vector<std::shared_ptr<const ObjectTemplate>> templates;
 	ui32 value = 0;
 	ui16 probability = 0;
 	ui32 maxPerZone = 1;
 	//ui32 maxPerMap; //unused
 	std::function<CGObjectInstance *()> generateObject;
+	std::function<void()> destroyObject;
 	
 	void setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrain);
 };

+ 1 - 1
lib/serializer/CSerializer.h

@@ -14,7 +14,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-const ui32 SERIALIZATION_VERSION = 831;
+const ui32 SERIALIZATION_VERSION = 832;
 const ui32 MINIMAL_SERIALIZATION_VERSION = 831;
 const std::string SAVEGAME_MAGIC = "VCMISVG";
 

+ 18 - 1
lib/spells/BattleSpellMechanics.cpp

@@ -213,7 +213,24 @@ bool BattleSpellMechanics::canBeCastAt(const Target & target, Problem & problem)
 
 	Target spellTarget = transformSpellTarget(target);
 
-    return effects->applicable(problem, this, target, spellTarget);
+	const battle::Unit * mainTarget = nullptr;
+
+	if (!getSpell()->canCastOnSelf())
+	{
+		if(spellTarget.front().unitValue)
+		{
+			mainTarget = target.front().unitValue;
+		}
+		else if(spellTarget.front().hexValue.isValid())
+		{
+			mainTarget = battle()->battleGetUnitByPos(target.front().hexValue, true);
+		}
+
+		if (mainTarget && mainTarget == caster)
+			return false; // can't cast on self
+	}
+
+	return effects->applicable(problem, this, target, spellTarget);
 }
 
 std::vector<const CStack *> BattleSpellMechanics::getAffectedStacks(const Target & target) const

+ 7 - 0
lib/spells/CSpellHandler.cpp

@@ -76,6 +76,7 @@ CSpell::CSpell():
 	power(0),
 	combat(false),
 	creatureAbility(false),
+	castOnSelf(false),
 	positiveness(ESpellPositiveness::NEUTRAL),
 	defaultProbability(0),
 	rising(false),
@@ -285,6 +286,11 @@ bool CSpell::hasBattleEffects() const
 	return levels[0].battleEffects.getType() == JsonNode::JsonType::DATA_STRUCT && !levels[0].battleEffects.Struct().empty();
 }
 
+bool CSpell::canCastOnSelf() const
+{
+	return castOnSelf;
+}
+
 const std::string & CSpell::getIconImmune() const
 {
 	return iconImmune;
@@ -702,6 +708,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode &
 		spell->school[info.id] = schoolNames[info.jsonName].Bool();
 	}
 
+	spell->castOnSelf = json["canCastOnSelf"].Bool();
 	spell->level = static_cast<si32>(json["level"].Integer());
 	spell->power = static_cast<si32>(json["power"].Integer());
 

+ 2 - 0
lib/spells/CSpellHandler.h

@@ -203,6 +203,7 @@ public:
 	int64_t calculateDamage(const spells::Caster * caster) const override;
 
 	bool hasSchool(SpellSchool school) const override;
+	bool canCastOnSelf() const override;
 
 	/**
 	 * Calls cb for each school this spell belongs to
@@ -329,6 +330,7 @@ private:
 	si32 power; //spell's power
 	bool combat; //is this spell combat (true) or adventure (false)
 	bool creatureAbility; //if true, only creatures can use this spell
+	bool castOnSelf; // if set, creature caster can cast this spell on itself
 	si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative
 
 	std::unique_ptr<spells::ISpellMechanicsFactory> mechanics;//(!) do not serialize

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

@@ -169,7 +169,6 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg
 			case 1: 
 				//Coronius style specialty bonus.
 				//Please note that actual Coronius isnt here, because Slayer is a spell that doesnt affect monster stats and is used only in calculateDmgRange
-				power = std::max(5 - tier, 0);
 				break;
 			}
 			if(m->isNegativeSpell())

+ 5 - 2
mapeditor/mapcontroller.cpp

@@ -131,7 +131,12 @@ void MapController::repairMap(CMap * map) const
 		//fix hero instance
 		if(auto * nih = dynamic_cast<CGHeroInstance*>(obj.get()))
 		{
+			// All heroes present on map or in prisons need to be allowed to rehire them after they are defeated
+
+			// FIXME: How about custom scenarios where defeated hero cannot be hired again?
+
 			map->allowedHeroes.insert(nih->getHeroType());
+
 			auto type = VLC->heroh->objects[nih->subID];
 			assert(type->heroClass);
 			//TODO: find a way to get proper type name
@@ -198,8 +203,6 @@ void MapController::repairMap(CMap * map) const
 				auto a = ArtifactUtils::createScroll(*RandomGeneratorUtil::nextItem(out, CRandomGenerator::getDefault()));
 				art->storedArtifact = a;
 			}
-			else
-				map->allowedArtifact.insert(art->getArtifact());
 		}
 	}
 }

+ 15 - 3
server/CGameHandler.cpp

@@ -1136,16 +1136,16 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 	};
 
 	if (guardian && getVisitingHero(guardian) != nullptr)
-		return complainRet("Cannot move hero, destination monster is busy!");
+		return complainRet("You cannot move your hero there. Simultaneous turns are active and another player is interacting with this wandering monster!");
 
 	if (objectToVisit && getVisitingHero(objectToVisit) != nullptr && getVisitingHero(objectToVisit) != h)
-		return complainRet("Cannot move hero, destination object is busy!");
+		return complainRet("You cannot move your hero there. Simultaneous turns are active and another player is interacting with this map object!");
 
 	if (objectToVisit &&
 		objectToVisit->getOwner().isValidPlayer() &&
 		getPlayerRelations(objectToVisit->getOwner(), h->getOwner()) == PlayerRelations::ENEMIES &&
 		!turnOrder->isContactAllowed(objectToVisit->getOwner(), h->getOwner()))
-		return complainRet("Cannot move hero, destination player is busy!");
+		return complainRet("You cannot move your hero there. This object belongs to another player and simultaneous turns are still active!");
 
 	//it's a rock or blocked and not visitable tile
 	//OR hero is on land and dest is water and (there is not present only one object - boat)
@@ -1467,6 +1467,9 @@ void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInsta
 	sendAndApply(&vc);
 	visitCastleObjects(obj, hero);
 	giveSpells (obj, hero);
+
+	if (obj->visitingHero && obj->garrisonHero)
+		useScholarSkill(obj->visitingHero->id, obj->garrisonHero->id);
 	checkVictoryLossConditionsForPlayer(hero->tempOwner); //transported artifact?
 }
 
@@ -1510,6 +1513,15 @@ void CGameHandler::setMovePoints(SetMovePoints * smp)
 	sendAndApply(smp);
 }
 
+void CGameHandler::setMovePoints(ObjectInstanceID hid, int val, bool absolute)
+{
+	SetMovePoints smp;
+	smp.hid = hid;
+	smp.val = val;
+	smp.absolute = absolute;
+	sendAndApply(&smp);
+}
+
 void CGameHandler::setManaPoints(ObjectInstanceID hid, int val)
 {
 	SetMana sm;

+ 3 - 2
server/CGameHandler.h

@@ -142,6 +142,7 @@ public:
 	bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override;
 	void giveHeroBonus(GiveBonus * bonus) override;
 	void setMovePoints(SetMovePoints * smp) override;
+	void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override;
 	void setManaPoints(ObjectInstanceID hid, int val) override;
 	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override;
 	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override;
@@ -248,7 +249,7 @@ public:
 
 	void wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer);
 	/// Unconditionally throws with "Action not allowed" message
-	void throwNotAllowedAction(CPackForServer * pack);
+	[[noreturn]] void throwNotAllowedAction(CPackForServer * pack);
 	/// Throws if player stated in pack is not making turn right now
 	void throwIfPlayerNotActive(CPackForServer * pack);
 	/// Throws if object is not owned by pack sender
@@ -256,7 +257,7 @@ public:
 	/// Throws if player is not present on connection of this pack
 	void throwIfWrongPlayer(CPackForServer * pack, PlayerColor player);
 	void throwIfWrongPlayer(CPackForServer * pack);
-	void throwAndComplain(CPackForServer * pack, std::string txt);
+	[[noreturn]] void throwAndComplain(CPackForServer * pack, std::string txt);
 
 	bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id);
 

+ 1 - 0
server/CVCMIServer.cpp

@@ -762,6 +762,7 @@ void CVCMIServer::updateAndPropagateLobbyState()
 		{
 			const auto & pset = psetPair.second;
 			si->mapGenOptions->setStartingTownForPlayer(pset.color, pset.castle);
+			si->mapGenOptions->setStartingHeroForPlayer(pset.color, pset.hero);
 			if(pset.isControlledByHuman())
 			{
 				si->mapGenOptions->setPlayerTypeForStandardPlayer(pset.color, EPlayerType::HUMAN);

+ 41 - 43
server/battles/BattleActionProcessor.cpp

@@ -268,7 +268,9 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c
 		totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, BonusSubtypeID(stack->creatureId()));
 	}
 
-	const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE);
+	static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeMelee));
+	const bool firstStrike = destinationStack->hasBonus(firstStrikeSelector);
+
 	const bool retaliation = destinationStack->ableToRetaliate();
 	bool ferocityApplied = false;
 	int32_t defenderInitialQuantity = destinationStack->getCount();
@@ -276,7 +278,7 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c
 	for (int i = 0; i < totalAttacks; ++i)
 	{
 		//first strike
-		if(i == 0 && firstStrike && retaliation)
+		if(i == 0 && firstStrike && retaliation && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION))
 		{
 			makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, false, true);
 		}
@@ -353,7 +355,11 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co
 		return false;
 	}
 
-	makeAttack(battle, stack, destinationStack, 0, destination, true, true, false);
+	static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeRanged));
+	const bool firstStrike = destinationStack->hasBonus(firstStrikeSelector);
+
+	if (!firstStrike)
+		makeAttack(battle, stack, destinationStack, 0, destination, true, true, false);
 
 	//ranged counterattack
 	if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION)
@@ -375,7 +381,7 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co
 		totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, BonusSubtypeID(stack->creatureId()));
 	}
 
-	for(int i = 1; i < totalRangedAttacks; ++i)
+	for(int i = firstStrike ? 0:1; i < totalRangedAttacks; ++i)
 	{
 		if(
 			stack->alive()
@@ -659,7 +665,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta
 
 	ret = path.second;
 
-	int creSpeed = curStack->speed(0, true);
+	int creSpeed = curStack->getMovementRange(0);
 
 	if (battle.battleGetTacticDist() > 0 && creSpeed > 0)
 		creSpeed = GameConstants::BFIELD_SIZE;
@@ -1139,18 +1145,10 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo
 			for(const auto & sf : *spellsByType)
 			{
 				int meleeRanged;
-				if(sf->additionalInfo.size() < 2)
-				{
-					// legacy format
-					vstd::amax(spellLevel, sf->additionalInfo[0] % 1000);
-					meleeRanged = sf->additionalInfo[0] / 1000;
-				}
-				else
-				{
-					vstd::amax(spellLevel, sf->additionalInfo[0]);
-					meleeRanged = sf->additionalInfo[1];
-				}
-				if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged))
+				vstd::amax(spellLevel, sf->additionalInfo[0]);
+				meleeRanged = sf->additionalInfo[1];
+
+				if (meleeRanged == CAddInfo::NONE || meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged))
 					castMe = true;
 			}
 			int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, BonusSubtypeID(spellID))));
@@ -1242,7 +1240,7 @@ void BattleActionProcessor::handleAttackBeforeCasting(const CBattleInfoCallback
 	attackCasting(battle, ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed?
 }
 
-void BattleActionProcessor::HandleDeathStareAndPirateShot(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender)
+void BattleActionProcessor::handleDeathStare(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender)
 {
 	// mechanics of Death Stare as in H3:
 	// each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution
@@ -1254,28 +1252,30 @@ void BattleActionProcessor::HandleDeathStareAndPirateShot(const CBattleInfoCallb
 		* X = 3 multiplier for shooting without penalty and X = 2 if shooting with penalty. Ability doesn't work if shooting at creatures behind walls.
 		*/
 
-	auto bonus = attacker->getBonus(Selector::type()(BonusType::DEATH_STARE));
-	if(bonus == nullptr)
-		bonus = attacker->getBonus(Selector::type()(BonusType::ACCURATE_SHOT));
+	auto subtype = BonusCustomSubtype::deathStareGorgon;
 
-	if(bonus->type == BonusType::ACCURATE_SHOT) //should not work from behind walls, except when being defender or under effect of golden bow etc.
+	if (ranged)
 	{
-		if(!ranged)
-			return;
-		if(battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition()))
-			return;
-	}
+		bool rangePenalty = battle.battleHasDistancePenalty(attacker, attacker->getPosition(), defender->getPosition());
+		bool obstaclePenalty = battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition());
 
-	int singleCreatureKillChancePercent;
-	if(bonus->type == BonusType::ACCURATE_SHOT)
-	{
-		singleCreatureKillChancePercent = attacker->valOfBonuses(BonusType::ACCURATE_SHOT);
-		if(battle.battleHasDistancePenalty(attacker, attacker->getPosition(), defender->getPosition()))
-			singleCreatureKillChancePercent = (singleCreatureKillChancePercent * 2) / 3;
+		if(rangePenalty)
+		{
+			if(obstaclePenalty)
+				subtype = BonusCustomSubtype::deathStareRangeObstaclePenalty;
+			else
+				subtype = BonusCustomSubtype::deathStareRangePenalty;
+		}
+		else
+		{
+			if(obstaclePenalty)
+				subtype = BonusCustomSubtype::deathStareObstaclePenalty;
+			else
+				subtype = BonusCustomSubtype::deathStareNoRangePenalty;
+		}
 	}
-	else //DEATH_STARE
-		singleCreatureKillChancePercent = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareGorgon);
 
+	int singleCreatureKillChancePercent = attacker->valOfBonuses(BonusType::DEATH_STARE, subtype);
 	double chanceToKill = singleCreatureKillChancePercent / 100.0;
 	vstd::amin(chanceToKill, 1); //cap at 100%
 	std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill);
@@ -1284,16 +1284,16 @@ void BattleActionProcessor::HandleDeathStareAndPirateShot(const CBattleInfoCallb
 	int maxToKill = (attacker->getCount() * singleCreatureKillChancePercent + 99) / 100;
 	vstd::amin(killedCreatures, maxToKill);
 
-	if(bonus->type == BonusType::DEATH_STARE)
-		killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level();
+	killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level();
 
 	if(killedCreatures)
 	{
 		//TODO: death stare or accurate shot was not originally available for multiple-hex attacks, but...
 
 		SpellID spellID = SpellID(SpellID::DEATH_STARE); //also used as fallback spell for ACCURATE_SHOT
-		if(bonus->type == BonusType::ACCURATE_SHOT && bonus->subtype.as<SpellID>() != SpellID::NONE)
-			spellID = bonus->subtype.as<SpellID>();
+		auto bonus = attacker->getBonus(Selector::typeSubtype(BonusType::DEATH_STARE, subtype));
+		if(bonus && bonus->additionalInfo[0] != SpellID::NONE)
+			spellID = SpellID(bonus->additionalInfo[0]);
 
 		const CSpell * spell = spellID.toSpell();
 		spells::AbilityCaster caster(attacker, 0);
@@ -1319,10 +1319,8 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback &
 		return;
 	}
 
-	if(attacker->hasBonusOfType(BonusType::DEATH_STARE) || attacker->hasBonusOfType(BonusType::ACCURATE_SHOT))
-	{
-		HandleDeathStareAndPirateShot(battle, ranged, attacker, defender);
-	}
+	if(attacker->hasBonusOfType(BonusType::DEATH_STARE))
+		handleDeathStare(battle, ranged, attacker, defender);
 
 	if(!defender->alive())
 		return;

+ 1 - 1
server/battles/BattleActionProcessor.h

@@ -45,7 +45,7 @@ class BattleActionProcessor : boost::noncopyable
 
 	void handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender);
 
-	void HandleDeathStareAndPirateShot(const CBattleInfoCallback &battle, bool ranged, const CStack *attacker, const CStack *defender);
+	void handleDeathStare(const CBattleInfoCallback &battle, bool ranged, const CStack *attacker, const CStack *defender);
 
 	void handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender);
 	void attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const CStack * defender);

+ 5 - 0
server/processors/HeroPoolProcessor.cpp

@@ -74,6 +74,7 @@ void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHer
 	sah.slotID = selectSlotForRole(color, sah.roleID);
 	sah.player = color;
 	sah.hid = hero->getHeroType();
+	sah.replenishPoints = false;
 	gameHandler->sendAndApply(&sah);
 }
 
@@ -87,6 +88,7 @@ void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroIns
 	sah.hid = hero->getHeroType();
 	sah.army.clearSlots();
 	sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1);
+	sah.replenishPoints = false;
 
 	gameHandler->sendAndApply(&sah);
 }
@@ -98,6 +100,7 @@ void HeroPoolProcessor::clearHeroFromSlot(const PlayerColor & color, TavernHeroS
 	sah.roleID = TavernSlotRole::NONE;
 	sah.slotID = slot;
 	sah.hid = HeroTypeID::NONE;
+	sah.replenishPoints = false;
 	gameHandler->sendAndApply(&sah);
 }
 
@@ -106,6 +109,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe
 	SetAvailableHero sah;
 	sah.player = color;
 	sah.slotID = slot;
+	sah.replenishPoints = true;
 
 	CGHeroInstance *newHero = pickHeroFor(needNativeHero, color);
 
@@ -129,6 +133,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe
 	{
 		sah.hid = HeroTypeID::NONE;
 	}
+
 	gameHandler->sendAndApply(&sah);
 }
 

+ 1 - 1
server/processors/PlayerMessageProcessor.cpp

@@ -384,7 +384,7 @@ void PlayerMessageProcessor::cheatPuzzleReveal(PlayerColor player)
 
 	for(auto & obj : gameHandler->gameState()->map->objects)
 	{
-		if(obj && obj->ID == Obj::OBELISK)
+		if(obj && obj->ID == Obj::OBELISK && !obj->wasVisited(player))
 		{
 			gameHandler->setObjPropertyID(obj->id, ObjProperty::OBELISK_VISITED, t->id);
 			for(const auto & color : t->players)

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно