Browse Source

Merge pull request #1776 from vcmi/beta

Merge beta -> develop
Ivan Savenko 2 years ago
parent
commit
9287d1b837
74 changed files with 1260 additions and 484 deletions
  1. 11 11
      AI/BattleAI/AttackPossibility.cpp
  2. 4 4
      AI/BattleAI/BattleExchangeVariant.cpp
  3. 2 2
      AI/BattleAI/StackWithBonuses.cpp
  4. 1 1
      AI/BattleAI/StackWithBonuses.h
  5. 4 3
      AI/StupidAI/StupidAI.cpp
  6. 289 0
      Mods/vcmi/config/vcmi/chinese.json
  7. 12 1
      Mods/vcmi/config/vcmi/english.json
  8. 76 9
      Mods/vcmi/config/vcmi/polish.json
  9. 11 0
      Mods/vcmi/config/vcmi/ukrainian.json
  10. 10 0
      Mods/vcmi/mod.json
  11. 6 3
      client/CPlayerInterface.cpp
  12. 1 1
      client/ClientCommandManager.h
  13. 14 2
      client/NetPacksClient.cpp
  14. 2 1
      client/adventureMap/CAdvMapInt.cpp
  15. 53 39
      client/adventureMap/CInGameConsole.cpp
  16. 26 7
      client/adventureMap/CInGameConsole.h
  17. 23 5
      client/adventureMap/CInfoBar.cpp
  18. 3 1
      client/adventureMap/CInfoBar.h
  19. 1 1
      client/adventureMap/CList.cpp
  20. 1 4
      client/adventureMap/CMinimap.cpp
  21. 7 1
      client/adventureMap/MapAudioPlayer.cpp
  22. 4 0
      client/adventureMap/MapAudioPlayer.h
  23. 167 76
      client/battle/BattleActionsController.cpp
  24. 4 4
      client/battle/BattleActionsController.h
  25. 1 1
      client/battle/BattleFieldController.cpp
  26. 34 20
      client/battle/BattleInterface.cpp
  27. 2 2
      client/battle/BattleInterface.h
  28. 23 12
      client/battle/BattleStacksController.cpp
  29. 3 1
      client/battle/BattleStacksController.h
  30. 3 2
      client/battle/BattleWindow.cpp
  31. 1 0
      client/battle/BattleWindow.h
  32. 9 8
      client/battle/CreatureAnimation.cpp
  33. 1 1
      client/gui/CGuiHandler.cpp
  34. 0 19
      client/gui/CIntObject.cpp
  35. 1 8
      client/gui/CIntObject.h
  36. 11 11
      client/mapView/IMapRendererObserver.h
  37. 4 2
      client/mapView/MapView.cpp
  38. 1 17
      client/mapView/MapViewActions.cpp
  39. 0 4
      client/mapView/MapViewActions.h
  40. 22 2
      client/mapView/MapViewController.cpp
  41. 2 1
      client/mapView/mapHandler.cpp
  42. 4 3
      client/render/IImage.h
  43. 13 4
      client/renderSDL/SDLImage.cpp
  44. 2 2
      client/renderSDL/SDLImage.h
  45. 2 1
      client/widgets/ObjectLists.cpp
  46. 2 1
      client/windows/CCastleInterface.cpp
  47. 3 0
      client/windows/CTradeWindow.cpp
  48. 1 0
      client/windows/CWindowObject.cpp
  49. 1 0
      cmake_modules/VCMI_lib.cmake
  50. 2 2
      config/battleEffects.json
  51. 2 2
      config/schemas/settings.json
  52. 51 51
      launcher/translation/polish.ts
  53. 1 1
      lib/CModHandler.cpp
  54. 2 2
      lib/GameConstants.h
  55. 4 4
      lib/battle/BattleInfo.cpp
  56. 1 1
      lib/battle/BattleInfo.h
  57. 48 26
      lib/battle/CBattleInfoCallback.cpp
  58. 6 31
      lib/battle/CBattleInfoCallback.h
  59. 3 0
      lib/battle/CUnitState.h
  60. 45 20
      lib/battle/DamageCalculator.cpp
  61. 9 4
      lib/battle/DamageCalculator.h
  62. 12 0
      lib/battle/IBattleInfoCallback.h
  63. 1 1
      lib/battle/IBattleState.h
  64. 78 0
      lib/battle/PossiblePlayerBattleAction.h
  65. 9 0
      lib/battle/Unit.h
  66. 3 3
      lib/mapObjects/CGTownInstance.cpp
  67. 3 2
      lib/mapObjects/CGTownInstance.h
  68. 34 3
      lib/mapObjects/CObjectClassesHandler.cpp
  69. 2 0
      lib/mapObjects/CObjectClassesHandler.h
  70. 5 0
      lib/mapObjects/ObjectTemplate.h
  71. 18 4
      lib/rmg/CMapGenerator.cpp
  72. 20 11
      lib/rmg/ConnectionsPlacer.cpp
  73. 20 15
      lib/rmg/WaterProxy.cpp
  74. 3 3
      server/CGameHandler.cpp

+ 11 - 11
AI/BattleAI/AttackPossibility.cpp

@@ -13,9 +13,9 @@
                               // Eventually only IBattleInfoCallback and battle::Unit should be used, 
                               // CUnitState should be private and CStack should be removed completely
 
-uint64_t averageDmg(const TDmgRange & range)
+uint64_t averageDmg(const DamageRange & range)
 {
-	return (range.first + range.second) / 2;
+	return (range.min + range.max) / 2;
 }
 
 AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
@@ -52,7 +52,7 @@ int64_t AttackPossibility::calculateDamageReduce(
 	// FIXME: provide distance info for Jousting bonus
 	auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attacker, 0);
 	auto enemiesKilled = damageDealt / defender->MaxHealth() + (damageDealt % defender->MaxHealth() >= defender->getFirstHPleft() ? 1 : 0);
-	auto enemyDamage = averageDmg(enemyDamageBeforeAttack);
+	auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage);
 	auto damagePerEnemy = enemyDamage / (double)defender->getCount();
 
 	return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->MaxHealth()));
@@ -85,7 +85,7 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a
 		auto rangeDmg = state.battleEstimateDamage(rangeAttackInfo);
 		auto meleeDmg = state.battleEstimateDamage(meleeAttackInfo);
 
-		int64_t gain = averageDmg(rangeDmg) - averageDmg(meleeDmg) + 1;
+		int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1;
 		res += gain;
 	}
 
@@ -156,16 +156,16 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 			{
 				int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce;
 
-				TDmgRange retaliation(0, 0);
+				DamageEstimation retaliation;
 				auto attackDmg = state.battleEstimateDamage(ap.attack, &retaliation);
 
-				vstd::amin(attackDmg.first, defenderState->getAvailableHealth());
-				vstd::amin(attackDmg.second, defenderState->getAvailableHealth());
+				vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth());
+				vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth());
 
-				vstd::amin(retaliation.first, ap.attackerState->getAvailableHealth());
-				vstd::amin(retaliation.second, ap.attackerState->getAvailableHealth());
+				vstd::amin(retaliation.damage.min, ap.attackerState->getAvailableHealth());
+				vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth());
 
-				damageDealt = averageDmg(attackDmg);
+				damageDealt = averageDmg(attackDmg.damage);
 				defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, state);
 				ap.attackerState->afterAttack(attackInfo.shooting, false);
 
@@ -175,7 +175,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 
 				if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked)
 				{
-					damageReceived = averageDmg(retaliation);
+					damageReceived = averageDmg(retaliation.damage);
 					attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, state);
 					defenderState->afterAttack(attackInfo.shooting, true);
 				}

+ 4 - 4
AI/BattleAI/BattleExchangeVariant.cpp

@@ -68,7 +68,7 @@ int64_t BattleExchangeVariant::trackAttack(
 	static const auto selectorBlocksRetaliation = Selector::type()(Bonus::BLOCKS_RETALIATION);
 	const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
 
-	TDmgRange retaliation;
+	DamageEstimation retaliation;
 	// FIXME: provide distance info for Jousting bonus
 	BattleAttackInfo bai(attacker.get(), defender.get(), 0, shooting);
 
@@ -78,7 +78,7 @@ int64_t BattleExchangeVariant::trackAttack(
 	}
 
 	auto attack = cb.battleEstimateDamage(bai, &retaliation);
-	int64_t attackDamage = (attack.first + attack.second) / 2;
+	int64_t attackDamage = (attack.damage.min + attack.damage.max) / 2;
 	int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, cb);
 	int64_t attackerDamageReduce = 0;
 
@@ -108,9 +108,9 @@ int64_t BattleExchangeVariant::trackAttack(
 
 	if(defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting)
 	{
-		if(retaliation.second != 0)
+		if(retaliation.damage.max != 0)
 		{
-			auto retaliationDamage = (retaliation.first + retaliation.second) / 2;
+			auto retaliationDamage = (retaliation.damage.min + retaliation.damage.max) / 2;
 			attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, cb);
 
 			if(!evaluateOnly)

+ 2 - 2
AI/BattleAI/StackWithBonuses.cpp

@@ -428,9 +428,9 @@ uint32_t HypotheticBattle::nextUnitId() const
 	return nextId++;
 }
 
-int64_t HypotheticBattle::getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const
+int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const
 {
-	return (damage.first + damage.second) / 2;
+	return (damage.min + damage.max) / 2;
 }
 
 int64_t HypotheticBattle::getTreeVersion() const

+ 1 - 1
AI/BattleAI/StackWithBonuses.h

@@ -138,7 +138,7 @@ public:
 
 	uint32_t nextUnitId() const override;
 
-	int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
+	int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
 
 	int64_t getTreeVersion() const;
 

+ 4 - 3
AI/StupidAI/StupidAI.cpp

@@ -56,9 +56,10 @@ public:
 	void calcDmg(const CStack * ourStack)
 	{
 		// FIXME: provide distance info for Jousting bonus
-		TDmgRange retal, dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal);
-		adi = static_cast<int>((dmg.first + dmg.second) / 2);
-		adr = static_cast<int>((retal.first + retal.second) / 2);
+		DamageEstimation retal;
+		DamageEstimation dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal);
+		adi = static_cast<int>((dmg.damage.min + dmg.damage.max) / 2);
+		adr = static_cast<int>((retal.damage.min + retal.damage.max) / 2);
 	}
 
 	bool operator==(const EnemyInfo& ei) const

+ 289 - 0
Mods/vcmi/config/vcmi/chinese.json

@@ -0,0 +1,289 @@
+{
+	"vcmi.adventureMap.monsterThreat.title"     : "\n\n 威胁等级: ",
+	"vcmi.adventureMap.monsterThreat.levels.0"  : "极低",
+	"vcmi.adventureMap.monsterThreat.levels.1"  : "很低",
+	"vcmi.adventureMap.monsterThreat.levels.2"  : "低",
+	"vcmi.adventureMap.monsterThreat.levels.3"  : "较低",
+	"vcmi.adventureMap.monsterThreat.levels.4"  : "中等",
+	"vcmi.adventureMap.monsterThreat.levels.5"  : "较高",
+	"vcmi.adventureMap.monsterThreat.levels.6"  : "高",
+	"vcmi.adventureMap.monsterThreat.levels.7"  : "很高",
+	"vcmi.adventureMap.monsterThreat.levels.8"  : "挑战性的",
+	"vcmi.adventureMap.monsterThreat.levels.9"  : "压倒性的",
+	"vcmi.adventureMap.monsterThreat.levels.10" : "致命的",
+	"vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜的",
+
+	"vcmi.adventureMap.confirmRestartGame"     : "你想要重新开始游戏吗?",
+	"vcmi.adventureMap.noTownWithMarket"       : "没有足够的市场。",
+	"vcmi.adventureMap.noTownWithTavern"       : "没有酒馆可供查看。",
+	"vcmi.adventureMap.spellUnknownProblem"    : "无此魔法的信息。",
+	"vcmi.adventureMap.playerAttacked"         : "玩家遭受攻击: %s",
+	"vcmi.adventureMap.moveCostDetails"        : "移动点数 - 花费: %TURNS 轮 + %POINTS 点移动力, 剩余移动力: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns" : "移动点数 - 花费: %POINTS 点移动力, 剩余移动力: %REMAINING",
+
+	"vcmi.server.errors.existingProcess"     : "另一个VCMI进程在运行,请结束当前进程。",
+	"vcmi.server.errors.modsIncompatibility" : "需要加载mod:",
+	"vcmi.server.confirmReconnect"           : "连接到上次吗?",
+
+	"vcmi.settingsMainWindow.generalTab.hover" : "常规",
+	"vcmi.settingsMainWindow.generalTab.help"     : "切换到“系统选项”选项卡 - 这些设置与常规游戏客户端行为相关",
+	"vcmi.settingsMainWindow.battleTab.hover" : "战斗",
+	"vcmi.settingsMainWindow.battleTab.help"     : "切换到“战斗选项”选项卡 - 这些设置允许配置战斗界面和相关内容",
+	"vcmi.settingsMainWindow.adventureTab.hover" : "冒险地图",
+	"vcmi.settingsMainWindow.adventureTab.help"  : "切换到“冒险地图”选项卡 - 冒险地图允许你移动英雄",
+	"vcmi.settingsMainWindow.otherTab.hover"     : "其他设置",
+	"vcmi.settingsMainWindow.otherTab.help"      : "切换到“其他设置”选项卡 - 由于各种原因,这些选项不适合其他类别",
+
+	"vcmi.systemOptions.fullscreenButton.hover" : "全屏",
+	"vcmi.systemOptions.fullscreenButton.help"  : "{全屏n}\n\n 当你选择全屏时,VCMI将会全屏运行,否则只会运行在指定框内",
+	"vcmi.systemOptions.resolutionButton.hover" : "分辨率",
+	"vcmi.systemOptions.resolutionButton.help"  : "{选择分辨率}\n\n 改变游戏的分辨率,达到更加清晰的效果。需要重新启动才能完成更改。",
+	"vcmi.systemOptions.resolutionMenu.hover"   : "选择分辨率",
+	"vcmi.systemOptions.resolutionMenu.help"    : "选择游戏的分辨率。",
+	"vcmi.systemOptions.fullscreenFailed"       : "{全屏}\n\n 选择切换到全屏失败!当前分辨率不支持全屏!",
+	"vcmi.systemOptions.framerateButton.hover"  : "显示传输帧数",
+	"vcmi.systemOptions.framerateButton.help"   : "{显示传输帧数}\n\n 打开/关闭在游戏窗口角落的传输帧数计数器。",
+
+	"vcmi.adventureOptions.numericQuantities.hover" : "生物数量显示",
+	"vcmi.adventureOptions.numericQuantities.help" : "{生物数量显示}\n\n 以数字 A-B 格式显示不准确的敌方生物数量。",
+	"vcmi.adventureOptions.forceMovementInfo.hover" : "在状态栏中显示移动力",
+	"vcmi.adventureOptions.forceMovementInfo.help" : "{在状态栏中显示移动力}\n\n 不需要按ALT就可以显示移动力。",
+	"vcmi.adventureOptions.showGrid.hover" : "显示六角网格",
+	"vcmi.adventureOptions.showGrid.help" : "{显示六角网格}\n\n 在战场上显示六角网格。",
+	"vcmi.adventureOptions.mapScrollSpeed4.hover": "4",
+	"vcmi.adventureOptions.mapScrollSpeed4.help": "设置动画速度为超快",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover": "5",
+	"vcmi.adventureOptions.mapScrollSpeed5.help": "设置动画速度为极速",
+
+	"vcmi.battleOptions.showQueue.hover": "显示移动次序",
+	"vcmi.battleOptions.showQueue.help": "{显示移动次序}\n\n 显示当前生物的移动次序。",
+	"vcmi.battleOptions.queueSizeLabel.hover": "次序条尺寸 (设置后下一场战斗生效)",
+	"vcmi.battleOptions.queueSizeAutoButton.hover": "自动设置尺寸",
+	"vcmi.battleOptions.queueSizeAutoButton.help": "根据游戏分辨率设置尺寸 (像素小于700为小尺寸,根据实际调整)",
+	"vcmi.battleOptions.queueSizeSmallButton.hover": "小尺寸",
+	"vcmi.battleOptions.queueSizeSmallButton.help": "设置次序条为小尺寸",
+	"vcmi.battleOptions.queueSizeBigButton.hover": "大尺寸",
+	"vcmi.battleOptions.queueSizeBigButton.help": "设置次寻条为大尺寸(不能在像素小于700时生效)",
+	"vcmi.battleOptions.animationsSpeed4.hover": "4",
+	"vcmi.battleOptions.animationsSpeed4.help": "设置动画速度为超快",
+	"vcmi.battleOptions.animationsSpeed5.hover": "5",
+	"vcmi.battleOptions.animationsSpeed5.help": "设置动画速度为极速",
+	"vcmi.battleOptions.animationsSpeed6.hover": "6",
+	"vcmi.battleOptions.animationsSpeed6.help": "设置动画速度为最快",
+	"vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过开场音乐",
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过开场音乐}\n\n 战斗开始时跳过开场音乐,直接按Esc也可以跳过。",
+
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "显示所有可以招募的城镇生物",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{显示所有可以招募的城镇生物}\n\n 显示当前所有可供招募的城镇生物 (左下角)。",
+	"vcmi.otherOptions.compactTownCreatureInfo.hover": "缩小城镇生物信息",
+	"vcmi.otherOptions.compactTownCreatureInfo.help": "{缩小城镇生物信息}\n\n 将城镇生物信息最小化.",
+
+	"vcmi.townHall.missingBase"             : "你必须先建造%s ",
+	"vcmi.townHall.noCreaturesToRecruit"    : "没有可供雇佣的生物。",
+	"vcmi.townHall.greetingManaVortex"      : "当你接近%s时,你的身体充满了新的能量。这使你的魔法值加倍。",
+	"vcmi.townHall.greetingKnowledge"       : "你学习了%s上的图形,并深入了解各种魔法的运作,这使你的知识点数+1。",
+	"vcmi.townHall.greetingSpellPower"      : "%s教你新的方法来集中你的魔法力量,这使你的力量点数+1。",
+	"vcmi.townHall.greetingExperience"      : "访问%s给你提供了更好的学习方法。这使你的经验值+1000。",
+	"vcmi.townHall.greetingAttack"          : "在%s参观后给你提供了更好的战斗技巧,这使你的攻击点数+1。",
+	"vcmi.townHall.greetingDefence"         : "在%s中度过一段时间后,经验丰富的勇士会教你额外的防御技能,这使你的防御点数+1。",
+	"vcmi.townHall.hasNotProduced"          : "本周%s并没有产生什么资源。",
+	"vcmi.townHall.hasProduced"             : "本周%s产生了%d个%s。",
+	"vcmi.townHall.greetingCustomBonus"     : "参观%s后,你的技巧有了提升。这使你受益匪浅。并且使你+%d %s%s",
+	"vcmi.townHall.greetingCustomUntil"     : "直到下一场战斗。",
+	"vcmi.townHall.greetingInTownMagicWell" : "%s使你的魔法值恢复到最大值。",
+
+	"vcmi.logicalExpressions.anyOf"  : "以下任何前提:",
+	"vcmi.logicalExpressions.allOf"  : "以下所有前提:",
+	"vcmi.logicalExpressions.noneOf" : "无前提:",
+
+	"vcmi.heroWindow.openCommander.hover" : "开启指挥官界面",
+	"vcmi.heroWindow.openCommander.help"  : "开启英雄的指挥官界面",
+
+	"vcmi.commanderWindow.artifactMessage" : "你要把这个宝物还给英雄吗?",
+
+	"vcmi.creatureWindow.showBonuses.hover"    : "属性界面",
+	"vcmi.creatureWindow.showBonuses.help"     : "显示指挥官的所有增强属性",
+	"vcmi.creatureWindow.showSkills.hover"     : "技能页面",
+	"vcmi.creatureWindow.showSkills.help"      : "显示指挥官的所有技能",
+	"vcmi.creatureWindow.returnArtifact.hover" : "交换宝物",
+	"vcmi.creatureWindow.returnArtifact.help"  : "将宝物还到英雄的背包里",
+
+	"vcmi.questLog.hideComplete.hover" : "隐藏完成任务",
+	"vcmi.questLog.hideComplete.help"  : "隐藏所有完成的任务",
+
+	"vcmi.randomMapTab.widgets.defaultTemplate"      : "默认",
+	"vcmi.randomMapTab.widgets.templateLabel"        : "格式",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设置...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "同盟关系",
+	
+	// few strings from WoG used by vcmi
+	"vcmi.stackExperience.description" : "» 经 验 获 得 明 细 «\n\n生物类型 ................... : %s\n经验等级 ................. : %s (%i)\n经验点数 ............... : %i\n下一个等级所需经验 .. : %i\n每次战斗最大获得经验 ... : %i%% (%i)\n获得经验的生物数量 .... : %i\n最大招募数量\n不会丢失经验升级 .... : %i\n经验倍数 ........... : %.2f\n升级倍数 .............. : %.2f\n10级后经验值 ........ : %i\n最大招募数量下\n 升级到10级所需经验数量: %i",
+	"vcmi.stackExperience.rank.1" : "新兵 1级",
+	"vcmi.stackExperience.rank.2" : "列兵 2级",
+	"vcmi.stackExperience.rank.3" : "下士 3级",
+	"vcmi.stackExperience.rank.4" : "中士 4级",
+	"vcmi.stackExperience.rank.5" : "上士 5级",
+	"vcmi.stackExperience.rank.6" : "少尉 6级",
+	"vcmi.stackExperience.rank.7" : "中尉 7级",
+	"vcmi.stackExperience.rank.8" : "上尉 8级",
+	"vcmi.stackExperience.rank.9" : "少校 9级",
+	"vcmi.stackExperience.rank.10" : "中校 10级",
+	"vcmi.stackExperience.rank.11" : "上校 11级",
+	
+	"core.bonus.ADDITIONAL_ATTACK.name": "双击",
+	"core.bonus.ADDITIONAL_ATTACK.description": "可以攻击两次",
+	"core.bonus.ADDITIONAL_RETALIATION.name": "额外反击",
+	"core.bonus.ADDITIONAL_RETALIATION.description": "可以额外反击 ${val} 次",
+	"core.bonus.AIR_IMMUNITY.name": "气系免疫",
+	"core.bonus.AIR_IMMUNITY.description": "免疫所有气系魔法",
+	"core.bonus.ATTACKS_ALL_ADJACENT.name": "环击",
+	"core.bonus.ATTACKS_ALL_ADJACENT.description": "攻击所有相邻部队",
+	"core.bonus.BLOCKS_RETALIATION.name": "无反击",
+	"core.bonus.BLOCKS_RETALIATION.description": "敌人无法反击",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.name": "远程无反击",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "敌人无法对射击进行反击",
+	"core.bonus.CATAPULT.name": "攻城",
+	"core.bonus.CATAPULT.description": "可以攻击城墙",
+	"core.bonus.CATAPULT_EXTRA_SHOTS.name": "额外攻击城墙",
+	"core.bonus.CATAPULT_EXTRA_SHOTS.description": "可以额外攻击城墙 ${val} 次",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "施法消耗 - (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "减少英雄的施法消耗",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "对方施法消耗 + (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "增加对方施法消耗",
+	"core.bonus.CHARGE_IMMUNITY.name": "I免疫冲锋",
+	"core.bonus.CHARGE_IMMUNITY.description": "对冲锋特技的额外伤害免疫",
+	"core.bonus.DARKNESS.name": "黑暗天幕",
+	"core.bonus.DARKNESS.description": "增加 ${val} 半径黑幕",
+	"core.bonus.DEATH_STARE.name": "死亡凝视 (${val}%)",
+	"core.bonus.DEATH_STARE.description": "${val}% 几率直接杀死生物",
+	"core.bonus.DEFENSIVE_STANCE.name": "防御奖励",
+	"core.bonus.DEFENSIVE_STANCE.description": "当选择防御时+${val} 防御力",
+	"core.bonus.DESTRUCTION.name": "毁灭",
+	"core.bonus.DESTRUCTION.description": "有${val}% 杀死额外数量的部队",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "致命一击",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% 几率造成双倍伤害",
+	"core.bonus.DRAGON_NATURE.name": "龙",
+	"core.bonus.DRAGON_NATURE.description": "生物属于龙类",
+	"core.bonus.DIRECT_DAMAGE_IMMUNITY.name": "魔法伤害免疫",
+	"core.bonus.DIRECT_DAMAGE_IMMUNITY.description": "对魔法伤害免疫",
+	"core.bonus.EARTH_IMMUNITY.name": "土系免疫",
+	"core.bonus.EARTH_IMMUNITY.description": "免疫所有土系魔法",
+	"core.bonus.ENCHANTER.name": "施法者",
+	"core.bonus.ENCHANTER.description": "每回合群体施放 ${subtype.spell} ",
+	"core.bonus.ENCHANTED.name": "魔法护身",
+	"core.bonus.ENCHANTED.description": "自身被 ${subtype.spell} 魔法影响",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "忽略防御 (${val}%)",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "攻击时忽略对方部分防御力",
+	"core.bonus.FIRE_IMMUNITY.name": "火系免疫",
+	"core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法",
+	"core.bonus.FIRE_SHIELD.name": "烈火神盾 (${val}%)",
+	"core.bonus.FIRE_SHIELD.description": "拥有烈火神盾护身",
+	"core.bonus.FIRST_STRIKE.name": "抢先攻击",
+	"core.bonus.FIRST_STRIKE.description": "在被反击前做出攻击",
+	"core.bonus.FEAR.name": "恐惧",
+	"core.bonus.FEAR.description": "引起恐惧",
+	"core.bonus.FEARLESS.name": "无惧",
+	"core.bonus.FEARLESS.description": "免疫恐惧",
+	"core.bonus.FLYING.name": "飞行兵种",
+	"core.bonus.FLYING.description": "生物可以飞行",
+	"core.bonus.FREE_SHOOTING.name": "近身射击",
+	"core.bonus.FREE_SHOOTING.description": "靠近敌方也能射击",
+	"core.bonus.FULL_HP_REGENERATION.name": "重生",
+	"core.bonus.FULL_HP_REGENERATION.description": "可以自动恢复所有生命值",
+	"core.bonus.GARGOYLE.name": "石像鬼属性",
+	"core.bonus.GARGOYLE.description": "不能被复活或治疗",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "减少伤害 (${val}%)",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "受攻击时减少受到的伤害",
+	"core.bonus.HATE.name": "${subtype.creature}的死敌",
+	"core.bonus.HATE.description": "对该部队造成 ${val}% 的额外伤害",
+	"core.bonus.HEALER.name": "治疗",
+	"core.bonus.HEALER.description": "可以治疗友军单位",
+	"core.bonus.HP_REGENERATION.name": "重生",
+	"core.bonus.HP_REGENERATION.description": "每回合恢复 ${val} 点生命值",
+	"core.bonus.JOUSTING.name": "冲锋",
+	"core.bonus.JOUSTING.description": "每格行动增加+5%伤害",
+	"core.bonus.KING1.name": "一般顶级怪物",
+	"core.bonus.KING1.description": "被初级屠戮成性影响",
+	"core.bonus.KING2.name": "智慧顶级怪物",
+	"core.bonus.KING2.description": "被中级屠戮成性影响",
+	"core.bonus.KING3.name": "精神顶级怪物",
+	"core.bonus.KING3.description":"被高级屠戮成性影响",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "免疫 1-${val} 级魔法",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "免疫等级为 1-${val} 级的所有魔法",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "半程射击",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "超过 ${val} 格不能射击",
+	"core.bonus.LIFE_DRAIN.name": "吸取生命 (${val}%)",
+	"core.bonus.LIFE_DRAIN.description": "吸取 ${val}% 伤害回复自身",
+	"core.bonus.MANA_CHANNELING.name": "偷取魔法 ${val}%",
+	"core.bonus.MANA_CHANNELING.description": "偷取部分敌人施法消耗",
+	"core.bonus.MANA_DRAIN.name": "吸取魔力",
+	"core.bonus.MANA_DRAIN.description": "每回合吸取 ${val} 魔法值",
+	"core.bonus.MAGIC_MIRROR.name": "带有魔法神镜 (${val}%)",
+	"core.bonus.MAGIC_MIRROR.description": "${val}% 几率反射魔法",
+	"core.bonus.MAGIC_RESISTANCE.name": "(${MR}%) 魔法抵抗",
+	"core.bonus.MAGIC_RESISTANCE.description": "${MR}% 几率抵抗敌人的魔法",
+	"core.bonus.MIND_IMMUNITY.name": "免疫心智",
+	"core.bonus.MIND_IMMUNITY.description": "不受心智魔法的影响",
+	"core.bonus.NO_DISTANCE_PENALTY.name": "无障碍射击",
+	"core.bonus.NO_DISTANCE_PENALTY.description": "射击不受距离影响",
+	"core.bonus.NO_MELEE_PENALTY.name": "无近战惩罚",
+	"core.bonus.NO_MELEE_PENALTY.description": "近战伤害不减",
+	"core.bonus.NO_MORALE.name": "无士气",
+	"core.bonus.NO_MORALE.description": "生物不受士气影响",
+	"core.bonus.NO_WALL_PENALTY.name": "无城墙影响",
+	"core.bonus.NO_WALL_PENALTY.description": "射击不受城墙的影响",
+	"core.bonus.NON_LIVING.name": "无生命",
+	"core.bonus.NON_LIVING.description": "不受只对生命实体生物有效的魔法",
+	"core.bonus.RANDOM_SPELLCASTER.name": "随机施法",
+	"core.bonus.RANDOM_SPELLCASTER.description": "随机施放增益魔法",
+	"core.bonus.RANGED_RETALIATION.name": "远程反击",
+	"core.bonus.RANGED_RETALIATION.description": "可以对远程攻击进行反击",
+	"core.bonus.RECEPTIVE.name": "接受有益魔法",
+	"core.bonus.RECEPTIVE.description": "不会免疫有益的魔法",
+	"core.bonus.REBIRTH.name": "复生 (${val}%)",
+	"core.bonus.REBIRTH.description": "{val}% 数量死亡后会复活",
+	"core.bonus.RETURN_AFTER_STRIKE.name": "攻击并返回",
+	"core.bonus.RETURN_AFTER_STRIKE.description": "攻击后回到初始位置",
+	"core.bonus.SELF_LUCK.name": "永久幸运",
+	"core.bonus.SELF_LUCK.description": "永久拥有幸运值",
+	"core.bonus.SELF_MORALE.name": "士气高涨",
+	"core.bonus.SELF_MORALE.description": "永久拥有高昂的士气",
+	"core.bonus.SHOOTER.name": "射手",
+	"core.bonus.SHOOTER.description": "生物可以设计",
+	"core.bonus.SHOOTS_ALL_ADJACENT.name": "范围远程攻击",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description": "远程攻击可伤害范围内的多个目标",
+	"core.bonus.SOUL_STEAL.name": "杀死敌人复生",
+	"core.bonus.SOUL_STEAL.description": "当杀死敌人时获得 ${val} 数量",
+	"core.bonus.SPELLCASTER.name": "施法者",
+	"core.bonus.SPELLCASTER.description": "生物可以施放 ${subtype.spell}",
+	"core.bonus.SPELL_AFTER_ATTACK.name": "攻击后施法",
+	"core.bonus.SPELL_AFTER_ATTACK.description": "${val}% 攻击后施放 ${subtype.spell}",
+	"core.bonus.SPELL_BEFORE_ATTACK.name": "攻击前施法",
+	"core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% 攻击前施放 ${subtype.spell}",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "魔法伤害抵抗",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "受魔法攻击时伤害减少 ${val}%",
+	"core.bonus.SPELL_IMMUNITY.name": "特定魔法免疫",
+	"core.bonus.SPELL_IMMUNITY.description": "免疫 ${subtype.spell}",
+	"core.bonus.SPELL_LIKE_ATTACK.name": "魔法攻击",
+	"core.bonus.SPELL_LIKE_ATTACK.description": "攻击时使用 ${subtype.spell}",
+	"core.bonus.SPELL_RESISTANCE_AURA.name": "抗魔光环",
+	"core.bonus.SPELL_RESISTANCE_AURA.description": "邻近部队获得 ${val}% 魔法抵抗",
+	"core.bonus.SUMMON_GUARDIANS.name": "召唤守卫",
+	"core.bonus.SUMMON_GUARDIANS.description": "战斗前召唤 ${subtype.creature} (${val}%)",
+	"core.bonus.SYNERGY_TARGET.name": "可协助攻击",
+	"core.bonus.SYNERGY_TARGET.description": "生物受到协助攻击的影响",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "龙息",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "吐息攻击2个部队",
+	"core.bonus.THREE_HEADED_ATTACK.name": "半环击",
+	"core.bonus.THREE_HEADED_ATTACK.description": "攻击正前方多个敌人",
+	"core.bonus.TRANSMUTATION.name": "变换",
+	"core.bonus.TRANSMUTATION.description": "${val}% 机会将敌人变成其他生物",
+	"core.bonus.UNDEAD.name": "不死生物",
+	"core.bonus.UNDEAD.description": "生物有丧尸属性",
+	"core.bonus.UNLIMITED_RETALIATIONS.name": "无限反击",
+	"core.bonus.UNLIMITED_RETALIATIONS.description": "每回合可以无限反击敌人",
+	"core.bonus.WATER_IMMUNITY.name": "水系免疫",
+	"core.bonus.WATER_IMMUNITY.description": "免疫水系魔法",
+	"core.bonus.WIDE_BREATH.name": "弧形焰息",
+	"core.bonus.WIDE_BREATH.description": "吐息攻击前方扇形6个部队"
+}

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

@@ -84,10 +84,21 @@
 	
 	"vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to skip battle intro",
 
+	"vcmi.battleWindow.damageEstimation.melee" : "Attack %CREATURE (%DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.meleeKills" : "Attack %CREATURE (%DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.ranged" : "Shoot %CREATURE (%SHOTS, %DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "Shoot %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.shots" : "%d shots left",
+	"vcmi.battleWindow.damageEstimation.shots.1" : "%d shot left",
+	"vcmi.battleWindow.damageEstimation.damage" : "%d damage",
+	"vcmi.battleWindow.damageEstimation.damage.1" : "%d damage",
+	"vcmi.battleWindow.damageEstimation.kills" : "%d will perish",
+	"vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish",
+
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Show Available Creatures",
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\n Shows creatures available to purchase instead of their growth in town summary (bottom-left corner).",
 	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Show Weekly Growth of Creatures",
-	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Show Weekly Growth of Creatures}\n\n Shows creatures' weekly growth instead of avaialable amount in town summary (bottom-left corner).",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Show Weekly Growth of Creatures}\n\n Shows creatures' weekly growth instead of available amount in town summary (bottom-left corner).",
 	"vcmi.otherOptions.compactTownCreatureInfo.hover": "Compact Creature Info",
 	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Compact Creature Info}\n\n Smaller town creatures information in town summary.",
 

+ 76 - 9
Mods/vcmi/config/vcmi/polish.json

@@ -1,5 +1,5 @@
 {
-	"vcmi.adventureMap.monsterThreat.title"     : "\n\n Zagrożenie: ",
+	"vcmi.adventureMap.monsterThreat.title"     : "\n\n Poziom zagrożenia: ",
 	"vcmi.adventureMap.monsterThreat.levels.0"  : "Zerowy",
 	"vcmi.adventureMap.monsterThreat.levels.1"  : "Bardzo słaby",
 	"vcmi.adventureMap.monsterThreat.levels.2"  : "Słaby",
@@ -25,12 +25,82 @@
 	"vcmi.server.errors.modsIncompatibility" : "Mody wymagane do wczytania gry:",
 	"vcmi.server.confirmReconnect"           : "Połączyć z ostatnią sesją?",
 
+	"vcmi.settingsMainWindow.generalTab.hover"   : "Ogólne",
+	"vcmi.settingsMainWindow.generalTab.help"    : "Przełącza do zakładki opcji ogólnych, która zawiera ustawienia związane z ogólnym działaniem gry",
+	"vcmi.settingsMainWindow.battleTab.hover"    : "Bitwa",
+	"vcmi.settingsMainWindow.battleTab.help"     : "Przełącza do zakładki opcji bitewnych, która pozwala konfigurować zachowanie gry w bitwach",
+	"vcmi.settingsMainWindow.adventureTab.hover" : "Mapa przygody",
+	"vcmi.settingsMainWindow.adventureTab.help"  : "Przełącza do zakładki opcji mapy przygody - mapa przygody to część gry, w której poruszasz bohaterami",
+
+	"vcmi.systemOptions.videoGroup" : "Ustawienia grafiki",
+	"vcmi.systemOptions.audioGroup" : "Ustawienia audio",
+	"vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now
+	"vcmi.systemOptions.townsGroup" : "Ekran miasta",
+
 	"vcmi.systemOptions.fullscreenButton.hover" : "Pełny ekran",
-	"vcmi.systemOptions.fullscreenButton.help"  : "{Fullscreen}\n\n Po wybraniu VCMI uruchomi się w trybie pełnoekranowym, w przeciwnym wypadku uruchomi się w oknie",
+	"vcmi.systemOptions.fullscreenButton.help"  : "{Pełny ekran}\n\n Po wybraniu VCMI uruchomi się w trybie pełnoekranowym, w przeciwnym wypadku uruchomi się w oknie",
 	"vcmi.systemOptions.resolutionButton.hover" : "Rozdzielczość: %wx%h",
-	"vcmi.systemOptions.resolutionButton.help"  : "{Select resolution}\n\n Zmień rozdzielczość ekranu w grze. Restart gry jest wymagany, by zmiany zostały uwzględnione.",
+	"vcmi.systemOptions.resolutionButton.help"  : "{Wybierz rozdzielczość}\n\n Zmień rozdzielczość ekranu w grze. Restart gry jest wymagany, by zmiany zostały uwzględnione.",
 	"vcmi.systemOptions.resolutionMenu.hover"   : "Wybierz rozdzielczość",
 	"vcmi.systemOptions.resolutionMenu.help"    : "Zmień rozdzielczość ekranu w grze.",
+	"vcmi.systemOptions.fullscreenFailed"       : "{Pełny ekran}\n\n Nieudane przełączenie w tryb pełnoekranowy! Obecna rozdzielczość nie jest wspierana przez wyświetlacz!",
+	"vcmi.systemOptions.framerateButton.hover"  : "Pokaż FPS",
+	"vcmi.systemOptions.framerateButton.help"   : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.",
+
+	"vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym",
+	"vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.",
+	"vcmi.adventureOptions.numericQuantities.hover" : "Liczbowe ilości stworzeń",
+	"vcmi.adventureOptions.numericQuantities.help" : "{Liczbowe ilości stworzeń}\n\n Pokazuje przybliżone ilości wrogów w liczbowym formacie A-B.",
+	"vcmi.adventureOptions.forceMovementInfo.hover" : "Zawsze pokazuj koszt ruchu",
+	"vcmi.adventureOptions.forceMovementInfo.help" : "{Zawsze pokazuj koszt ruchu}\n\n Zastępuje domyślne informacje paska statusu danymi o ruchu bez potrzeby przytrzymywania klawisza ALT.",
+	"vcmi.adventureOptions.showGrid.hover" : "Pokaż siatkę",
+	"vcmi.adventureOptions.showGrid.help" : "{Pokaż siatkę}\n\n Włącza siatkę pokazującą brzegi pól mapy przygody.",
+	"vcmi.adventureOptions.mapSwipe.hover" : "Przeciąganie mapy",
+	"vcmi.adventureOptions.mapSwipe.help" : "{Przeciąganie mapy}\n\n Pozwala przesuwać mapę przygody palcem dla systemów z ekranami dotykowymi. Obecnie pozwala też przesuwać mapę lewym przyciskiem myszy.",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed1.help": "Ustaw szybkość przesuwania mapy na bardzo wolną.",
+	"vcmi.adventureOptions.mapScrollSpeed5.help": "Ustaw szybkość przesuwania mapy na bardzo szybką.",
+	"vcmi.adventureOptions.mapScrollSpeed6.help": "Ustaw szybkość przesuwania mapy na błyskawiczną.",
+
+	"vcmi.battleOptions.queueSizeLabel.hover": "Pokaż kolejkę ruchu jednostek",
+	"vcmi.battleOptions.queueSizeNoneButton.hover": "BRAK",
+	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
+	"vcmi.battleOptions.queueSizeSmallButton.hover": "MAŁA",
+	"vcmi.battleOptions.queueSizeBigButton.hover": "DUŻA",
+	"vcmi.battleOptions.queueSizeNoneButton.help": "Kompletnie wyłącza widoczność kolejki ruchu jednostek",
+	"vcmi.battleOptions.queueSizeAutoButton.help": "Ustawia rozmiar kolejki zależnie od rozdzielczości gry (mała dla rozdzielczości z wysokością poniżej 700 pikseli, duża dla pozostałych)",
+	"vcmi.battleOptions.queueSizeSmallButton.help": "Ustawia rozmiar kolejki na mały",
+	"vcmi.battleOptions.queueSizeBigButton.help": "Ustawia rozmiar kolejki na duży (nie wspierane dla rozdzielczości z wysokością mniejszą niż 700 pikseli)",
+	"vcmi.battleOptions.animationsSpeed1.hover": "",
+	"vcmi.battleOptions.animationsSpeed5.hover": "",
+	"vcmi.battleOptions.animationsSpeed6.hover": "",
+	"vcmi.battleOptions.animationsSpeed1.help": "Ustawia szybkość animacji na bardzo wolną",
+	"vcmi.battleOptions.animationsSpeed5.help": "Ustawia szybkość animacji na bardzo szybką",
+	"vcmi.battleOptions.animationsSpeed6.help": "Ustawia szybkość animacji na błyskawiczną",
+	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń muzykę startową",
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń muzykę startową}\n\n Pomija krótką muzykę, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji. Może również być pominięta poprzez naciśnięcie ESC.",
+
+	"vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by pominąć muzykę startową",
+
+	"vcmi.battleWindow.damageEstimation.melee" : "Atakuj %CREATURE (%DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.meleeKills" : "Atakuj %CREATURE (%DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.ranged" : "Strzelaj do %CREATURE (%SHOTS, %DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "Strzelaj do %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.shots" : "pozostałe strzały: %d",
+	"vcmi.battleWindow.damageEstimation.shots.1" : "pozostał %d strzał",
+	"vcmi.battleWindow.damageEstimation.damage" : "obrażenia: %d",
+	"vcmi.battleWindow.damageEstimation.damage.1" : "obrażenia: %d",
+	"vcmi.battleWindow.damageEstimation.kills" : "%d zginie",
+	"vcmi.battleWindow.damageEstimation.kills.1" : "%d zginie",
+
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Pokaż dostępne stworzenia",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Pokaż dostępne stworzenia}\n\n Pokazuje dostępne stworzenia zamiast tygodniowego przyrostu w podsumowaniu miasta (lewy dolny róg).",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Pokaż tygodniowy przyrost stworzeń",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Pokaż tygodniowy przyrost stworzeń}\n\n Shows creatures' weekly growth instead of avaialable amount in town summary (lewy dolny r óg).",
+	"vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompaktowa informacja o stworzeniu",
+	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompaktowa informacja o stworzeniu}\n\n Zmniejszona informacja o stworzeniu w podsumowaniu miasta.",
 
 	"vcmi.townHall.missingBase"             : "Podstawowy budynek %s musi zostać najpierw wybudowany",
 	"vcmi.townHall.noCreaturesToRecruit"    : "Brak stworzeń do rekrutacji!",
@@ -69,6 +139,7 @@
 	"vcmi.randomMapTab.widgets.templateLabel"        : "Szablon",
 	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Ustaw...",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Sojusze",
+	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Typy dróg",
 
 	"core.bonus.ADDITIONAL_ATTACK.name": "Podwójne Uderzenie",
 	"core.bonus.ADDITIONAL_ATTACK.description": "Atakuje podwójnie",
@@ -140,12 +211,8 @@
 	"core.bonus.HP_REGENERATION.description": "Leczy ${SHval} punktów zdrowia każdej rundy",
 	"core.bonus.JOUSTING.name": "Szarża Czempiona",
 	"core.bonus.JOUSTING.description": "+${val}% obrażeń na przebytego heksa",
-	"core.bonus.KING1.name": "Król 1",
-	"core.bonus.KING1.description": "Wrażliwy na podstawowy czar POGROMCA",
-	"core.bonus.KING2.name": "Król 2",
-	"core.bonus.KING2.description": "Wrażliwy na zaawansowany czar POGROMCA",
-	"core.bonus.KING3.name": "Król 3",
-	"core.bonus.KING3.description":"Wrażliwy na ekspercki czar POGROMCA",
+	"core.bonus.KING.name": "Król",
+	"core.bonus.KING.description": "Wrażliwy na czar POGROMCA stopnia zaawansowania ${val} lub wyższego",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odporność na czary 1-${val}",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odporny na czary 1-${val} poziomu",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Ograniczony zasięg strzelania",

+ 11 - 0
Mods/vcmi/config/vcmi/ukrainian.json

@@ -84,6 +84,17 @@
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.",
 	
 	"vcmi.battleWindow.pressKeyToSkipIntro" : "Натисніть будь-яку клавішу, щоб розпочати бій",
+	
+	"vcmi.battleWindow.damageEstimation.melee" : "Атакувати %CREATURE (%DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.meleeKills" : "Атакувати %CREATURE (%DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.ranged" : "Стріляти в %CREATURE (%SHOTS, %DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "Стріляти в %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.shots" : "%d пострілів залишилось",
+	"vcmi.battleWindow.damageEstimation.shots.1" : "%d постріл залишився",
+	"vcmi.battleWindow.damageEstimation.damage" : "%d одиниць пошкоджень",
+	"vcmi.battleWindow.damageEstimation.damage.1" : "%d одиниця пошкодження",
+	"vcmi.battleWindow.damageEstimation.kills" : "%d загинуть",
+	"vcmi.battleWindow.damageEstimation.kills.1" : "%d загине",
 
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Показувати доступних істот",
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Показувати доступних істот}\n\n Показує істот, яких можна придбати, замість їхнього приросту у зведенні по місту (нижній лівий кут).",

+ 10 - 0
Mods/vcmi/mod.json

@@ -1,6 +1,16 @@
 {
 	"name" : "VCMI essential files",
 	"description" : "Essential files required for VCMI to run correctly",
+
+	"chinese" : {
+		"name" : "VCMI essential files",
+		"description" : "Essential files required for VCMI to run correctly",
+		
+		"skipValidation" : true,
+		"translations" : [
+			"config/vcmi/chinese.json"
+		]
+	},
 	
 	"german" : {
 		"name" : "VCMI - grundlegende Dateien",

+ 6 - 3
client/CPlayerInterface.cpp

@@ -583,6 +583,7 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town)
 		castleInt->garr->setArmy(town->visitingHero, 1);
 		castleInt->garr->recreateSlots();
 		castleInt->heroes->update();
+		castleInt->redraw();
 	}
 	for (auto isa : GH.listInt)
 	{
@@ -591,9 +592,9 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town)
 		{
 			ki->townChanged(town);
 			ki->updateGarrisons();
+			ki->redraw();
 		}
 	}
-	GH.totalRedraw();
 }
 void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town)
 {
@@ -1510,6 +1511,7 @@ void CPlayerInterface::objectRemoved(const CGObjectInstance * obj)
 		const CGHeroInstance * h = static_cast<const CGHeroInstance *>(obj);
 		heroKilled(h);
 	}
+	GH.fakeMouseMove();
 }
 
 void CPlayerInterface::objectRemovedAfter()
@@ -1559,7 +1561,6 @@ void CPlayerInterface::update()
 	}
 
 	assert(adventureInt);
-	assert(adventureInt->selection);
 
 	// Handles mouse and key input
 	GH.updateTime();
@@ -2032,7 +2033,9 @@ bool CPlayerInterface::capturedAllEvents()
 		return true;
 	}
 
-	if (ignoreEvents)
+	bool needToLockAdventureMap = adventureInt->active && CGI->mh->hasOngoingAnimations();
+
+	if (ignoreEvents || needToLockAdventureMap)
 	{
 		boost::unique_lock<boost::mutex> un(eventsM);
 		while(!SDLEventsQueue.empty())

+ 1 - 1
client/ClientCommandManager.h

@@ -17,7 +17,7 @@ class CIntObject;
 
 class ClientCommandManager //take mantis #2292 issue about account if thinking about handling cheats from command-line
 {
-	bool currentCallFromIngameConsole;
+	bool currentCallFromIngameConsole = false;
 
 	void giveTurn(const PlayerColor &color);
 	void printInfoAboutInterfaceObject(const CIntObject *obj, int level);

+ 14 - 2
client/NetPacksClient.cpp

@@ -514,6 +514,11 @@ void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack)
 	{
 		callInterfaceIfPresent(cl, town->tempOwner, &IGameEventsReceiver::buildChanged, town, id, 1);
 	}
+
+	// invalidate section of map view with our object and force an update
+	CGI->mh->onObjectInstantRemove(town);
+	CGI->mh->onObjectInstantAdd(town);
+
 }
 void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack)
 {
@@ -522,6 +527,10 @@ void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack)
 	{
 		callInterfaceIfPresent(cl, town->tempOwner, &IGameEventsReceiver::buildChanged, town, id, 2);
 	}
+
+	// invalidate section of map view with our object and force an update
+	CGI->mh->onObjectInstantRemove(town);
+	CGI->mh->onObjectInstantAdd(town);
 }
 
 void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack)
@@ -607,7 +616,7 @@ void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack)
 
 	if (pack.what == ObjProperty::OWNER)
 	{
-		// invalidate section of map view with our objec and force an update with new flag color
+		// invalidate section of map view with our object and force an update with new flag color
 		CGI->mh->onObjectInstantRemove(gs.getObjInstance(pack.id));
 		CGI->mh->onObjectInstantAdd(gs.getObjInstance(pack.id));
 	}
@@ -739,11 +748,14 @@ void ApplyFirstClientNetPackVisitor::visitBattleStackMoved(BattleStackMoved & pa
 void ApplyFirstClientNetPackVisitor::visitBattleAttack(BattleAttack & pack)
 {
 	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleAttack, &pack);
+
+	// battleStacksAttacked should be excuted before BattleAttack.applyGs() to play animation before damaging unit
+	// so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack()
+	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, pack.bsa, pack.shot());
 }
 
 void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, pack.bsa, pack.shot());
 }
 
 void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack)

+ 2 - 1
client/adventureMap/CAdvMapInt.cpp

@@ -986,7 +986,6 @@ void CAdvMapInt::initializeNewTurn()
 {
 	heroList->update();
 	townList->update();
-	mapAudio->onPlayerTurnStarted();
 
 	const CGHeroInstance * heroToSelect = nullptr;
 
@@ -1017,6 +1016,7 @@ void CAdvMapInt::initializeNewTurn()
 
 	updateNextHero(nullptr);
 	showAll(screen);
+	mapAudio->onPlayerTurnStarted();
 
 	if(settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown())
 	{
@@ -1404,6 +1404,7 @@ void CAdvMapInt::aiTurnStarted()
 	mapAudio->onEnemyTurnStarted();
 	adventureInt->minimap->setAIRadar(true);
 	adventureInt->infoBar->startEnemyTurn(LOCPLINT->cb->getCurrentPlayer());
+	adventureInt->minimap->showAll(screen);//force refresh on inactive object
 	adventureInt->infoBar->showAll(screen);//force refresh on inactive object
 }
 

+ 53 - 39
client/adventureMap/CInGameConsole.cpp

@@ -24,71 +24,85 @@
 #include "../../lib/TextOperations.h"
 #include "../../lib/mapObjects/CArmedInstance.h"
 
-#include <SDL_timer.h>
-
 CInGameConsole::CInGameConsole()
-	: CIntObject(KEYBOARD | TEXTINPUT),
-	prevEntDisp(-1),
-	defaultTimeout(10000),
-	maxDisplayedTexts(10)
+	: CIntObject(KEYBOARD | TIME | TEXTINPUT)
+	, prevEntDisp(-1)
+{
+	type |= REDRAW_PARENT;
+}
+
+void CInGameConsole::showAll(SDL_Surface * to)
 {
+	show(to);
 }
 
 void CInGameConsole::show(SDL_Surface * to)
 {
 	int number = 0;
 
-	std::vector<std::list< std::pair< std::string, uint32_t > >::iterator> toDel;
-
 	boost::unique_lock<boost::mutex> lock(texts_mx);
-	for(auto it = texts.begin(); it != texts.end(); ++it, ++number)
+	for(auto & text : texts)
 	{
 		Point leftBottomCorner(0, pos.h);
+		Point textPosition(leftBottomCorner.x + 50, leftBottomCorner.y - texts.size() * 20 - 80 + number * 20);
 
-		graphics->fonts[FONT_MEDIUM]->renderTextLeft(to, it->first, Colors::GREEN,
-			Point(leftBottomCorner.x + 50, leftBottomCorner.y - (int)texts.size() * 20 - 80 + number*20));
+		graphics->fonts[FONT_MEDIUM]->renderTextLeft(to, text.text, Colors::GREEN, textPosition );
 
-		if((int)(SDL_GetTicks() - it->second) > defaultTimeout)
-		{
-			toDel.push_back(it);
-		}
+		number++;
 	}
+}
 
-	for(auto & elem : toDel)
+void CInGameConsole::tick(uint32_t msPassed)
+{
+	size_t sizeBefore = texts.size();
 	{
-		texts.erase(elem);
+		boost::unique_lock<boost::mutex> lock(texts_mx);
+
+		for(auto & text : texts)
+			text.timeOnScreen += msPassed;
+
+		vstd::erase_if(
+			texts,
+			[&](const auto & value)
+			{
+				return value.timeOnScreen > defaultTimeout;
+			}
+		);
 	}
+
+	if(sizeBefore != texts.size())
+		GH.totalRedraw(); // FIXME: ingame console has no parent widget set
 }
 
-void CInGameConsole::print(const std::string &txt)
+void CInGameConsole::print(const std::string & txt)
 {
-	boost::unique_lock<boost::mutex> lock(texts_mx);
-	int lineLen = conf.go()->ac.outputLineLength;
-
-	if(txt.size() < lineLen)
+	// boost::unique_lock scope
 	{
-		texts.push_back(std::make_pair(txt, SDL_GetTicks()));
-		if(texts.size() > maxDisplayedTexts)
+		boost::unique_lock<boost::mutex> lock(texts_mx);
+		int lineLen = conf.go()->ac.outputLineLength;
+
+		if(txt.size() < lineLen)
 		{
-			texts.pop_front();
+			texts.push_back({txt, 0});
 		}
-	}
-	else
-	{
-		assert(lineLen);
-		for(int g=0; g<txt.size() / lineLen + 1; ++g)
+		else
 		{
-			std::string part = txt.substr(g * lineLen, lineLen);
-			if(part.size() == 0)
-				break;
-
-			texts.push_back(std::make_pair(part, SDL_GetTicks()));
-			if(texts.size() > maxDisplayedTexts)
+			assert(lineLen);
+			for(int g = 0; g < txt.size() / lineLen + 1; ++g)
 			{
-				texts.pop_front();
+				std::string part = txt.substr(g * lineLen, lineLen);
+				if(part.empty())
+					break;
+
+				texts.push_back({part, 0});
 			}
 		}
+
+		while(texts.size() > maxDisplayedTexts)
+			texts.erase(texts.begin());
 	}
+
+	GH.totalRedraw(); // FIXME: ingame console has no parent widget set
 }
 
 void CInGameConsole::keyPressed (const SDL_Keycode & key)
@@ -136,7 +150,7 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key)
 		}
 	case SDLK_UP: //up arrow
 		{
-			if(previouslyEntered.size() == 0)
+			if(previouslyEntered.empty())
 				break;
 
 			if(prevEntDisp == -1)
@@ -178,7 +192,7 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key)
 
 void CInGameConsole::textInputed(const std::string & inputtedText)
 {
-	if(!captureAllKeys || enteredText.size() == 0)
+	if(!captureAllKeys || enteredText.empty())
 		return;
 	enteredText.resize(enteredText.size()-1);
 

+ 26 - 7
client/adventureMap/CInGameConsole.h

@@ -14,20 +14,39 @@
 class CInGameConsole : public CIntObject
 {
 private:
-	std::list< std::pair< std::string, uint32_t > > texts; //list<text to show, time of add>
-	boost::mutex texts_mx;		// protects texts
-	std::vector< std::string > previouslyEntered; //previously entered texts, for up/down arrows to work
-	int prevEntDisp; //displayed entry from previouslyEntered - if none it's -1
-	int defaultTimeout; //timeout for new texts (in ms)
-	int maxDisplayedTexts; //hiw many texts can be displayed simultaneously
+	struct TextState
+	{
+		std::string text;
+		uint32_t timeOnScreen;
+	};
+
+	/// Currently visible texts in the overlay
+	std::vector<TextState> texts;
+
+	/// protects texts
+	boost::mutex texts_mx;
+
+	/// previously entered texts, for up/down arrows to work
+	std::vector<std::string> previouslyEntered;
+
+	/// displayed entry from previouslyEntered - if none it's -1
+	int prevEntDisp;
+
+	/// timeout for new texts (in ms)
+	static constexpr int defaultTimeout = 10000;
+
+	/// how many texts can be displayed simultaneously
+	static constexpr int maxDisplayedTexts = 10;
 
 	std::weak_ptr<IStatusBar> currentStatusBar;
 	std::string enteredText;
 
 public:
-	void print(const std::string &txt);
+	void print(const std::string & txt);
 
+	void tick(uint32_t msPassed) override;
 	void show(SDL_Surface * to) override;
+	void showAll(SDL_Surface * to) override;
 	void keyPressed(const SDL_Keycode & key) override;
 	void textInputed(const std::string & enteredText) override;
 	void textEdited(const std::string & enteredText) override;

+ 23 - 5
client/adventureMap/CInfoBar.cpp

@@ -254,11 +254,21 @@ void CInfoBar::showSelection()
 	showGameStatus();//FIXME: may be incorrect but shouldn't happen in general
 }
 
-void CInfoBar::tick()
+void CInfoBar::tick(uint32_t msPassed)
 {
-	removeUsedEvents(TIME);
-	if(GH.topInt() == adventureInt)
-		popComponents(true);
+	assert(timerCounter > 0);
+
+	if (msPassed >= timerCounter)
+	{
+		timerCounter = 0;
+		removeUsedEvents(TIME);
+		if(GH.topInt() == adventureInt)
+			popComponents(true);
+	}
+	else
+	{
+		timerCounter -= msPassed;
+	}
 }
 
 void CInfoBar::clickLeft(tribool down, bool previousState)
@@ -290,6 +300,7 @@ void CInfoBar::hover(bool on)
 
 CInfoBar::CInfoBar(const Rect & position)
 	: CIntObject(LCLICK | RCLICK | HOVER, position.topLeft()),
+	timerCounter(0),
 	state(EMPTY)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
@@ -302,6 +313,14 @@ CInfoBar::CInfoBar(const Point & position): CInfoBar(Rect(position.x, position.y
 {
 }
 
+
+void CInfoBar::setTimer(uint32_t msToTrigger)
+{
+	if (!(active & TIME))
+		addUsedEvents(TIME);
+	timerCounter = msToTrigger;
+}
+
 void CInfoBar::showDate()
 {
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
@@ -312,7 +331,6 @@ void CInfoBar::showDate()
 	redraw();
 }
 
-
 void CInfoBar::pushComponents(const std::vector<Component> & components, std::string message, int timer)
 {
 	auto actualPush = [&](const std::vector<Component> & components, std::string message, int timer, size_t max){

+ 3 - 1
client/adventureMap/CInfoBar.h

@@ -138,6 +138,7 @@ private:
 
 	std::shared_ptr<CVisibleInfo> visibleInfo;
 	EState state;
+	uint32_t timerCounter;
 	bool shouldPopAll = false;
 
 	std::queue<std::pair<VisibleComponentInfo::Cache, int>> componentsQueue;
@@ -151,13 +152,14 @@ private:
 	//removes all information about current state, deactivates timer (if any)
 	void reset();
 
-	void tick() override;
+	void tick(uint32_t msPassed) override;
 
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
 	void hover(bool on) override;
 
 	void playNewDaySound();
+	void setTimer(uint32_t msToTrigger);
 public:
 	CInfoBar(const Rect & pos);
 	CInfoBar(const Point & pos);

+ 1 - 1
client/adventureMap/CList.cpp

@@ -77,7 +77,7 @@ void CList::CListItem::onSelect(bool on)
 	if(on)
 		selection = genSelection();
 	select(on);
-	GH.totalRedraw();
+	redraw();
 }
 
 CList::CList(int Size, Point position, std::string btnUp, std::string btnDown, size_t listAmount, int helpUp, int helpDown, CListBox::CreateFunc create)

+ 1 - 4
client/adventureMap/CMinimap.cpp

@@ -222,10 +222,7 @@ void CMinimap::setAIRadar(bool on)
 		aiShield->disable();
 		update();
 	}
-
-	// this may happen during AI turn when this interface is inactive
-	// force redraw in order to properly update interface
-	GH.totalRedraw();
+	redraw();
 }
 
 void CMinimap::updateTile(const int3 &pos)

+ 7 - 1
client/adventureMap/MapAudioPlayer.cpp

@@ -131,7 +131,11 @@ std::vector<std::string> MapAudioPlayer::getAmbientSounds(const int3 & tile)
 	{
 		const auto & object = CGI->mh->getMap()->objects[objectID.getNum()];
 
-		if(object->getAmbientSound())
+		assert(object);
+		if (!object)
+			logGlobal->warn("Already removed object %d found on tile! (%d %d %d)", objectID.getNum(), tile.x, tile.y, tile.z);
+
+		if(object && object->getAmbientSound())
 			result.push_back(object->getAmbientSound().get());
 	}
 
@@ -194,8 +198,10 @@ MapAudioPlayer::MapAudioPlayer()
 	objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]);
 
 	for(const auto & obj : CGI->mh->getMap()->objects)
+	{
 		if (obj)
 			addObject(obj);
+	}
 }
 
 MapAudioPlayer::~MapAudioPlayer()

+ 4 - 0
client/adventureMap/MapAudioPlayer.h

@@ -47,6 +47,10 @@ protected:
 	void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override;
 	void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override;
 
+	void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {}
+	void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {}
+	void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {}
+
 public:
 	MapAudioPlayer();
 	~MapAudioPlayer() override;

+ 167 - 76
client/battle/BattleActionsController.cpp

@@ -25,25 +25,95 @@
 #include "../windows/CCreatureWindow.h"
 
 #include "../../CCallback.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CStack.h"
 #include "../../lib/battle/BattleAction.h"
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/spells/Problem.h"
-#include "../../lib/CGeneralTextHandler.h"
 
-static std::string formatDmgRange(std::pair<ui32, ui32> dmgRange)
+struct TextReplacement
 {
-	if (dmgRange.first != dmgRange.second)
-		return (boost::format("%d - %d") % dmgRange.first % dmgRange.second).str();
-	else
-		return (boost::format("%d") % dmgRange.first).str();
+	std::string placeholder;
+	std::string replacement;
+};
+
+using TextReplacementList = std::vector<TextReplacement>;
+
+static std::string replacePlaceholders(std::string input, const TextReplacementList & format )
+{
+	for(const auto & entry : format)
+		boost::replace_all(input, entry.placeholder, entry.replacement);
+
+	return input;
+}
+
+static std::string translatePlural(int amount, const std::string& baseTextID)
+{
+	if(amount == 1)
+		return CGI->generaltexth->translate(baseTextID + ".1");
+	return CGI->generaltexth->translate(baseTextID);
+}
+
+static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID)
+{
+	std::string baseString = translatePlural(amount, baseTextID);
+	TextReplacementList replacements {
+		{ "%d", amountString }
+	};
+
+	return replacePlaceholders(baseString, replacements);
+}
+
+static std::string formatPlural(int amount, const std::string & baseTextID)
+{
+	return formatPluralImpl(amount, std::to_string(amount), baseTextID);
+}
+
+static std::string formatPlural(DamageRange range, const std::string & baseTextID)
+{
+	if (range.min == range.max)
+		return formatPlural(range.min, baseTextID);
+
+	std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max);
+
+	return formatPluralImpl(range.max, rangeString, baseTextID);
+}
+
+static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft)
+{
+	TextReplacementList replacements = {
+		{ "%CREATURE", creatureName },
+		{ "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") },
+		{ "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") },
+		{ "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") },
+	};
+
+	return replacePlaceholders(CGI->generaltexth->translate(baseTextID), replacements);
+}
+
+static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName)
+{
+	std::string baseTextID = estimation.kills.max == 0 ?
+		"vcmi.battleWindow.damageEstimation.melee" :
+		"vcmi.battleWindow.damageEstimation.meleeKills";
+
+	return formatAttack(estimation, creatureName, baseTextID, 0);
+}
+
+static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft)
+{
+	std::string baseTextID = estimation.kills.max == 0 ?
+		"vcmi.battleWindow.damageEstimation.ranged" :
+		"vcmi.battleWindow.damageEstimation.rangedKills";
+
+	return formatAttack(estimation, creatureName, baseTextID, shotsLeft);
 }
 
 BattleActionsController::BattleActionsController(BattleInterface & owner):
 	owner(owner),
-	heroSpellToCast(nullptr),
-	creatureSpellToCast(nullptr)
+	heroSpellToCast(nullptr)
 {}
 
 void BattleActionsController::endCastingSpell()
@@ -63,8 +133,8 @@ bool BattleActionsController::isActiveStackSpellcaster() const
 	if (!casterStack)
 		return false;
 
-	const auto randomSpellcaster = casterStack->getBonusLocalFirst(Selector::type()(Bonus::SPELLCASTER));
-	return (randomSpellcaster && casterStack->canCast());
+	bool spellcaster = casterStack->hasBonusOfType(Bonus::SPELLCASTER);
+	return (spellcaster && casterStack->canCast());
 }
 
 void BattleActionsController::enterCreatureCastingMode()
@@ -83,10 +153,13 @@ void BattleActionsController::enterCreatureCastingMode()
 	if (!isActiveStackSpellcaster())
 		return;
 
-	if (vstd::contains(possibleActions, PossiblePlayerBattleAction::NO_LOCATION))
+	for (auto const & action : possibleActions)
 	{
+		if (action.get() != PossiblePlayerBattleAction::NO_LOCATION)
+			continue;
+
 		const spells::Caster * caster = owner.stacksController->getActiveStack();
-		const CSpell * spell = getStackSpellToCast();
+		const CSpell * spell = action.spell().toSpell();
 
 		spells::Target target;
 		target.emplace_back();
@@ -105,31 +178,26 @@ void BattleActionsController::enterCreatureCastingMode()
 
 			CCS->curh->set(Cursor::Combat::POINTER);
 		}
+		return;
 	}
-	else
-	{
-		possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack());
 
-		auto actionFilterPredicate = [](const PossiblePlayerBattleAction x)
-		{
-			return (x != PossiblePlayerBattleAction::ANY_LOCATION) && (x != PossiblePlayerBattleAction::NO_LOCATION) &&
-				(x != PossiblePlayerBattleAction::FREE_LOCATION) && (x != PossiblePlayerBattleAction::AIMED_SPELL_CREATURE) &&
-				(x != PossiblePlayerBattleAction::OBSTACLE);
-		};
+	possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack());
 
-		vstd::erase_if(possibleActions, actionFilterPredicate);
-		GH.fakeMouseMove();
-	}
+	auto actionFilterPredicate = [](const PossiblePlayerBattleAction x)
+	{
+		return !x.spellcast();
+	};
+
+	vstd::erase_if(possibleActions, actionFilterPredicate);
+	GH.fakeMouseMove();
 }
 
 std::vector<PossiblePlayerBattleAction> BattleActionsController::getPossibleActionsForStack(const CStack *stack) const
 {
 	BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass
 
-	if (getStackSpellToCast())
-		data.creatureSpellToCast = getStackSpellToCast()->getId();
-	else
-		data.creatureSpellToCast = SpellID::NONE;
+	for (auto const & spell : creatureSpells)
+		data.creatureSpellsToCast.push_back(spell->id);
 
 	data.tacticsMode = owner.tacticsMode;
 	auto allActions = owner.curInt->cb->getClientActionsForStack(stack, data);
@@ -146,7 +214,7 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac
 
 	auto assignPriority = [&](PossiblePlayerBattleAction const & item) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it
 	{
-		switch(item)
+		switch(item.get())
 		{
 		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
 		case PossiblePlayerBattleAction::ANY_LOCATION:
@@ -207,7 +275,7 @@ void BattleActionsController::castThisSpell(SpellID spellID)
 	assert(castingHero); // code below assumes non-null hero
 	PossiblePlayerBattleAction spellSelMode = owner.curInt->cb->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO);
 
-	if (spellSelMode == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location
+	if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location
 	{
 		heroSpellToCast->aimToHex(BattleHex::INVALID);
 		owner.curInt->cb->battleMakeAction(heroSpellToCast.get());
@@ -228,19 +296,30 @@ const CSpell * BattleActionsController::getHeroSpellToCast( ) const
 	return nullptr;
 }
 
-const CSpell * BattleActionsController::getStackSpellToCast( ) const
+const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex)
 {
-	if (isActiveStackSpellcaster())
-		return creatureSpellToCast;
+	if (heroSpellToCast)
+		return nullptr;
 
-	return nullptr;
+	if (!owner.stacksController->getActiveStack())
+		return nullptr;
+
+	if (!hoveredHex.isValid())
+		return nullptr;
+
+	auto action = selectAction(hoveredHex);
+
+	if (action.spell() == SpellID::NONE)
+		return nullptr;
+
+	return action.spell().toSpell();
 }
 
-const CSpell * BattleActionsController::getCurrentSpell( ) const
+const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex)
 {
 	if (getHeroSpellToCast())
 		return getHeroSpellToCast();
-	return getStackSpellToCast();
+	return getStackSpellToCast(hoveredHex);
 }
 
 const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex)
@@ -253,7 +332,7 @@ const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex)
 
 void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex)
 {
-	switch (action)
+	switch (action.get())
 	{
 		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
 			CCS->curh->set(Cursor::Combat::POINTER);
@@ -316,7 +395,7 @@ void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action,
 
 void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex targetHex)
 {
-	switch (action)
+	switch (action.get())
 	{
 		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
 		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
@@ -339,7 +418,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 {
 	const CStack * targetStack = getStackForHex(targetHex);
 
-	switch (action) //display console message, realize selected action
+	switch (action.get()) //display console message, realize selected action
 	{
 		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
 			return (boost::format(CGI->generaltexth->allTexts[481]) % targetStack->getName()).str(); //Select %s
@@ -356,26 +435,29 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
 			{
 				BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
-				TDmgRange damage = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex);
-				std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg
-				return (boost::format(CGI->generaltexth->allTexts[36]) % targetStack->getName() % estDmgText).str(); //Attack %s (%s damage)
+				DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex);
+				estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
+				estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
+
+				return formatMeleeAttack(estimation, targetStack->getName());
 			}
 
 		case PossiblePlayerBattleAction::SHOOT:
 		{
-			auto const * shooter = owner.stacksController->getActiveStack();
+			const auto * shooter = owner.stacksController->getActiveStack();
+
+			DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(shooter, targetStack, shooter->getPosition());
+			estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
+			estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
 
-			TDmgRange damage = owner.curInt->cb->battleEstimateDamage(shooter, targetStack, shooter->getPosition());
-			std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg
-			//printing - Shoot %s (%d shots left, %s damage)
-			return (boost::format(CGI->generaltexth->allTexts[296]) % targetStack->getName() % shooter->shots.available() % estDmgText).str();
+			return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available());
 		}
 
 		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-			return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % getCurrentSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s
+			return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % action.spell().toSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s
 
 		case PossiblePlayerBattleAction::ANY_LOCATION:
-			return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % getCurrentSpell()->getNameTranslated()); //Cast %s
+			return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s
 
 		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
 			return boost::str(boost::format(CGI->generaltexth->allTexts[301]) % targetStack->getName()); //Cast a spell on %
@@ -390,7 +472,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 			return (boost::format(CGI->generaltexth->allTexts[549]) % targetStack->getName()).str(); //sacrifice the %s
 
 		case PossiblePlayerBattleAction::FREE_LOCATION:
-			return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % getCurrentSpell()->getNameTranslated()); //Cast %s
+			return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s
 
 		case PossiblePlayerBattleAction::HEAL:
 			return (boost::format(CGI->generaltexth->allTexts[419]) % targetStack->getName()).str(); //Apply first aid to the %s
@@ -410,7 +492,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 
 std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex targetHex)
 {
-	switch (action)
+	switch (action.get())
 	{
 		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
 		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
@@ -423,7 +505,7 @@ std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlaye
 			return CGI->generaltexth->allTexts[543]; //choose army to sacrifice
 			break;
 		case PossiblePlayerBattleAction::FREE_LOCATION:
-			return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % getCurrentSpell()->getNameTranslated()); //No room to place %s here
+			return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % action.spell().toSpell()->getNameTranslated()); //No room to place %s here
 			break;
 		default:
 			return "";
@@ -435,7 +517,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 	const CStack * targetStack = getStackForHex(targetHex);
 	bool targetStackOwned = targetStack && targetStack->owner == owner.curInt->playerID;
 
-	switch (action)
+	switch (action.get())
 	{
 		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
 		case PossiblePlayerBattleAction::CREATURE_INFO:
@@ -472,11 +554,14 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 		case PossiblePlayerBattleAction::SHOOT:
 			return owner.curInt->cb->battleCanShoot(owner.stacksController->getActiveStack(), targetHex);
 
+		case PossiblePlayerBattleAction::NO_LOCATION:
+			return false;
+
 		case PossiblePlayerBattleAction::ANY_LOCATION:
-			return isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex);
+			return isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex);
 
 		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-			return targetStack && isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex);
+			return targetStack && isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex);
 
 		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
 			if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures
@@ -497,8 +582,8 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 
 		case PossiblePlayerBattleAction::OBSTACLE:
 		case PossiblePlayerBattleAction::FREE_LOCATION:
-			return isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex);
-			return isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex);
+			return isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex);
+			return isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex);
 
 		case PossiblePlayerBattleAction::CATAPULT:
 			return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex);
@@ -515,7 +600,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 {
 	const CStack * targetStack = getStackForHex(targetHex);
 
-	switch (action) //display console message, realize selected action
+	switch (action.get()) //display console message, realize selected action
 	{
 		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
 		{
@@ -546,7 +631,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
 		{
-			bool returnAfterAttack = action == PossiblePlayerBattleAction::ATTACK_AND_RETURN;
+			bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN;
 			BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
 			if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308)
 			{
@@ -599,16 +684,16 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 		case PossiblePlayerBattleAction::SACRIFICE:
 		case PossiblePlayerBattleAction::FREE_LOCATION:
 		{
-			if (action == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE )
+			if (action.get() == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE )
 			{
-				if (getCurrentSpell()->id == SpellID::SACRIFICE)
+				if (action.spell() == SpellID::SACRIFICE)
 				{
 					heroSpellToCast->aimToHex(targetHex);
 					possibleActions.push_back(PossiblePlayerBattleAction::SACRIFICE);
 					owner.stacksController->setSelectedStack(targetStack);
 					return;
 				}
-				if (getCurrentSpell()->id == SpellID::TELEPORT)
+				if (action.spell() == SpellID::TELEPORT)
 				{
 					heroSpellToCast->aimToUnit(targetStack);
 					possibleActions.push_back(PossiblePlayerBattleAction::TELEPORT);
@@ -619,9 +704,9 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 
 			if (!spellcastingModeActive())
 			{
-				if (getStackSpellToCast())
+				if (action.spell().toSpell())
 				{
-					owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, getStackSpellToCast()->getId());
+					owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell());
 				}
 				else //unknown random spell
 				{
@@ -750,12 +835,25 @@ void BattleActionsController::onHexLeftClicked(BattleHex clickedHex)
 
 void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack)
 {
-	const auto spellcaster = casterStack->getBonusLocalFirst(Selector::type()(Bonus::SPELLCASTER));
+	creatureSpells.clear();
+
+	bool spellcaster = casterStack->hasBonusOfType(Bonus::SPELLCASTER);
 	if(casterStack->canCast() && spellcaster)
 	{
 		// faerie dragon can cast only one, randomly selected spell until their next move
 		//TODO: faerie dragon type spell should be selected by server
-		creatureSpellToCast = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell();
+		const auto * spellToCast = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell();
+
+		if (spellToCast)
+			creatureSpells.push_back(spellToCast);
+	}
+
+	TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(Bonus::SPELLCASTER));
+
+	for (auto const & bonus : *bl)
+	{
+		if (bonus->additionalInfo[0] <= 0)
+			creatureSpells.push_back(SpellID(bonus->subtype).toSpell());
 	}
 }
 
@@ -776,11 +874,9 @@ spells::Mode BattleActionsController::getCurrentCastMode() const
 
 }
 
-bool BattleActionsController::isCastingPossibleHere(const CStack *casterStack, const CStack *targetStack, BattleHex targetHex)
+bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *casterStack, const CStack *targetStack, BattleHex targetHex)
 {
-	auto currentSpell = getCurrentSpell();
 	assert(currentSpell);
-
 	if (!currentSpell)
 		return false;
 
@@ -823,7 +919,7 @@ void BattleActionsController::activateStack()
 		std::list<PossiblePlayerBattleAction> actionsToSelect;
 		if(!possibleActions.empty())
 		{
-			switch(possibleActions.front())
+			switch(possibleActions.front().get())
 			{
 				case PossiblePlayerBattleAction::SHOOT:
 					actionsToSelect.push_back(possibleActions.front());
@@ -873,12 +969,7 @@ bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex)
 
 	auto action = selectAction(hoveredHex);
 
-	return
-		action == PossiblePlayerBattleAction::ANY_LOCATION ||
-		action == PossiblePlayerBattleAction::NO_LOCATION ||
-		action == PossiblePlayerBattleAction::FREE_LOCATION ||
-		action == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE ||
-		action == PossiblePlayerBattleAction::OBSTACLE;
+	return action.spellcast();
 }
 
 const std::vector<PossiblePlayerBattleAction> & BattleActionsController::getPossibleActions() const

+ 4 - 4
client/battle/BattleActionsController.h

@@ -45,9 +45,9 @@ class BattleActionsController
 	std::string currentConsoleMsg;
 
 	/// if true, active stack could possibly cast some target spell
-	const CSpell * creatureSpellToCast;
+	std::vector<const CSpell *> creatureSpells;
 
-	bool isCastingPossibleHere (const CStack *sactive, const CStack *shere, BattleHex myNumber);
+	bool isCastingPossibleHere (const CSpell * spell, const CStack *sactive, const CStack *shere, BattleHex myNumber);
 	bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber) const; //TODO: move to BattleState / callback
 	std::vector<PossiblePlayerBattleAction> getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn
 	void reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context);
@@ -74,7 +74,7 @@ class BattleActionsController
 	const CSpell * getHeroSpellToCast() const;
 
 	/// if current stack is spellcaster, returns spell being cast, or null othervice
-	const CSpell * getStackSpellToCast( ) const;
+	const CSpell * getStackSpellToCast(BattleHex hoveredHex);
 
 	/// returns true if current stack is a spellcaster
 	bool isActiveStackSpellcaster() const;
@@ -116,7 +116,7 @@ public:
 	void onHexRightClicked(BattleHex clickedHex);
 
 	const spells::Caster * getCurrentSpellcaster() const;
-	const CSpell * getCurrentSpell() const;
+	const CSpell * getCurrentSpell(BattleHex hoveredHex);
 	spells::Mode getCurrentCastMode() const;
 
 	/// methods to work with array of possible actions, needed to control special creatures abilities

+ 1 - 1
client/battle/BattleFieldController.cpp

@@ -263,7 +263,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesSpellRange()
 	const CSpell *spell = nullptr;
 
 	spells::Mode mode = owner.actionsController->getCurrentCastMode();
-	spell = owner.actionsController->getCurrentSpell();
+	spell = owner.actionsController->getCurrentSpell(hoveredHex);
 	caster = owner.actionsController->getCurrentSpellcaster();
 
 	if(caster && spell) //when casting spell

+ 34 - 20
client/battle/BattleInterface.cpp

@@ -54,6 +54,7 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 	, attackerInt(att)
 	, defenderInt(defen)
 	, curInt(att)
+	, battleOpeningDelayActive(true)
 {
 	if(spectatorInt)
 	{
@@ -112,7 +113,7 @@ void BattleInterface::playIntroSoundAndUnlockInterface()
 		}
 	};
 
-	battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds);
+	int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds);
 	if (battleIntroSoundChannel != -1)
 	{
 		CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);
@@ -120,8 +121,15 @@ void BattleInterface::playIntroSoundAndUnlockInterface()
 		if (settings["gameTweaks"]["skipBattleIntroMusic"].Bool())
 			openingEnd();
 	}
-	else
+	else // failed to play sound
+	{
 		onIntroSoundPlayed();
+	}
+}
+
+bool BattleInterface::openingPlaying()
+{
+	return battleOpeningDelayActive;
 }
 
 void BattleInterface::onIntroSoundPlayed()
@@ -132,6 +140,19 @@ void BattleInterface::onIntroSoundPlayed()
 	CCS->musich->playMusicFromSet("battle", true, true);
 }
 
+void BattleInterface::openingEnd()
+{
+	assert(openingPlaying());
+	if (!openingPlaying())
+		return;
+
+	onAnimationsFinished();
+	if(tacticsMode)
+		tacticNextStack(nullptr);
+	activateStack();
+	battleOpeningDelayActive = false;
+}
+
 BattleInterface::~BattleInterface()
 {
 	CPlayerInterface::battleInt = nullptr;
@@ -310,6 +331,12 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 {
 	windowObject->blockUI(true);
 
+	// Disable current active stack duing the cast
+	// Store the current activeStack to stackToActivate
+	stacksController->deactivateStack();
+
+	CCS->curh->set(Cursor::Combat::BLOCKED);
+
 	const SpellID spellID = sc->spellID;
 	const CSpell * spell = spellID.toSpell();
 	auto targetedTile = sc->tile;
@@ -524,24 +551,6 @@ void BattleInterface::activateStack()
 	GH.fakeMouseMove();
 }
 
-bool BattleInterface::openingPlaying()
-{
-	return battleIntroSoundChannel != -1;
-}
-
-void BattleInterface::openingEnd()
-{
-	assert(openingPlaying());
-	if (!openingPlaying())
-		return;
-
-	onAnimationsFinished();
-	if(tacticsMode)
-		tacticNextStack(nullptr);
-	activateStack();
-	battleIntroSoundChannel = -1;
-}
-
 bool BattleInterface::makingTurn() const
 {
 	return stacksController->getActiveStack() != nullptr;
@@ -551,6 +560,9 @@ void BattleInterface::endAction(const BattleAction* action)
 {
 	const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber);
 
+	// Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast
+	activateStack();
+
 	stacksController->endAction(action);
 	windowObject->updateQueue();
 
@@ -693,6 +705,8 @@ void BattleInterface::requestAutofightingAIToTakeAction()
 				auto ba = std::make_unique<BattleAction>(curInt->autofightingAI->activeStack(activeStack));
 				givenCommand.setn(ba.release());
 			}
+
+			stacksController->setActiveStack(nullptr);
 		}
 	});
 

+ 2 - 2
client/battle/BattleInterface.h

@@ -111,8 +111,8 @@ class BattleInterface
 	/// defender interface, not null if attacker is human in our vcmiclient
 	std::shared_ptr<CPlayerInterface> defenderInt;
 
-	/// ID of channel on which battle opening sound is playing, or -1 if none
-	int battleIntroSoundChannel;
+	/// if set to true, battle is still starting and waiting for intro sound to end / key press from player
+	bool battleOpeningDelayActive;
 
 	void playIntroSoundAndUnlockInterface();
 	void onIntroSoundPlayed();

+ 23 - 12
client/battle/BattleStacksController.cpp

@@ -88,16 +88,13 @@ BattleStacksController::BattleStacksController(BattleInterface & owner):
 	static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f );
 	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f );
 
-	amountNormal->adjustPalette(shifterNormal, 0);
-	amountPositive->adjustPalette(shifterPositive, 0);
-	amountNegative->adjustPalette(shifterNegative, 0);
-	amountEffNeutral->adjustPalette(shifterNeutral, 0);
+	// do not change border color
+	static const int32_t ignoredMask = 1 << 26;
 
-	//Restore border color {255, 231, 132, 255} to its original state
-	amountNormal->resetPalette(26);
-	amountPositive->resetPalette(26);
-	amountNegative->resetPalette(26);
-	amountEffNeutral->resetPalette(26);
+	amountNormal->adjustPalette(shifterNormal, ignoredMask);
+	amountPositive->adjustPalette(shifterPositive, ignoredMask);
+	amountNegative->adjustPalette(shifterNegative, ignoredMask);
+	amountEffNeutral->adjustPalette(shifterNeutral, ignoredMask);
 
 	std::vector<const CStack*> stacks = owner.curInt->cb->battleGetAllStacks(true);
 	for(const CStack * s : stacks)
@@ -187,7 +184,7 @@ void BattleStacksController::stackReset(const CStack * stack)
 void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 {
 	// Tower shooters have only their upper half visible
-	static const int turretCreatureAnimationHeight = 225;
+	static const int turretCreatureAnimationHeight = 232;
 
 	stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER; // must be set before getting stack position
 
@@ -201,6 +198,11 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 
 		stackAnimation[stack->ID] = AnimationControls::getAnimation(turretCreature);
 		stackAnimation[stack->ID]->pos.h = turretCreatureAnimationHeight;
+		stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth();
+
+		// FIXME: workaround for visible animation of Medusa tails (animation disabled in H3)
+		if (turretCreature->idNumber == CreatureID::MEDUSA )
+			stackAnimation[stack->ID]->pos.w = 250;
 
 		coords = owner.siegeController->getTurretCreaturePosition(stack->initialPosition);
 	}
@@ -209,10 +211,10 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 		stackAnimation[stack->ID] = AnimationControls::getAnimation(stack->getCreature());
 		stackAnimation[stack->ID]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->ID]);
 		stackAnimation[stack->ID]->pos.h = stackAnimation[stack->ID]->getHeight();
+		stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth();
 	}
 	stackAnimation[stack->ID]->pos.x = coords.x;
 	stackAnimation[stack->ID]->pos.y = coords.y;
-	stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth();
 	stackAnimation[stack->ID]->setType(ECreatureAnimType::HOLDING);
 
 	if (!instant)
@@ -683,6 +685,15 @@ void BattleStacksController::stackActivated(const CStack *stack)
 	owner.activateStack();
 }
 
+void BattleStacksController::deactivateStack()
+{
+	if (!activeStack) {
+		return;
+	}
+	stackToActivate = activeStack;
+	setActiveStack(nullptr);
+}
+
 void BattleStacksController::activateStack()
 {
 	if ( !currentAnimations.empty())
@@ -840,7 +851,7 @@ std::vector<const CStack *> BattleStacksController::selectHoveredStacks()
 	const CSpell *spell = nullptr;
 
 	spells::Mode mode = owner.actionsController->getCurrentCastMode();
-	spell = owner.actionsController->getCurrentSpell();
+	spell = owner.actionsController->getCurrentSpell(hoveredHex);
 	caster = owner.actionsController->getCurrentSpellcaster();
 
 	if(caster && spell && owner.actionsController->currentActionSpellcasting(hoveredHex) ) //when casting spell

+ 3 - 1
client/battle/BattleStacksController.h

@@ -118,7 +118,9 @@ public:
 	void startAction(const BattleAction* action);
 	void endAction(const BattleAction* action);
 
-	void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all
+	void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack
+
+	void activateStack(); //copy stackToActivate to activeStack to enable controls of the stack
 
 	void setActiveStack(const CStack *stack);
 	void setSelectedStack(const CStack *stack);

+ 3 - 2
client/battle/BattleWindow.cpp

@@ -38,7 +38,8 @@
 #include "../windows/settings/SettingsMainWindow.h"
 
 BattleWindow::BattleWindow(BattleInterface & owner):
-	owner(owner)
+	owner(owner),
+	defaultAction(PossiblePlayerBattleAction::INVALID)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	pos.w = 800;
@@ -326,7 +327,7 @@ void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action)
 		return;
 	
 	std::string iconName = variables["actionIconDefault"].String();
-	switch(action)
+	switch(action.get())
 	{
 		case PossiblePlayerBattleAction::ATTACK:
 			iconName = variables["actionIconAttack"].String();

+ 1 - 0
client/battle/BattleWindow.h

@@ -12,6 +12,7 @@
 #include "../gui/CIntObject.h"
 #include "../gui/InterfaceObjectConfigurable.h"
 #include "../../lib/battle/CBattleInfoCallback.h"
+#include "../../lib/battle/PossiblePlayerBattleAction.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 class CStack;

+ 9 - 8
client/battle/CreatureAnimation.cpp

@@ -343,13 +343,14 @@ static SDL_Color addColors(const SDL_Color & base, const SDL_Color & over)
 
 void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target)
 {
-	target[0] = genShadow(shadowAlpha / 2);
+	target.resize(8);
+	target[0] = genShadow(0);
 	target[1] = genShadow(shadowAlpha / 2);
-	target[2] = genShadow(shadowAlpha);
-	target[3] = genShadow(shadowAlpha);
-	target[4] = genBorderColor(getBorderStrength(elapsedTime), border);
-	target[5] = addColors(genShadow(shadowAlpha),     genBorderColor(getBorderStrength(elapsedTime), border));
-	target[6] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border));
+	// colors 2 & 3 are not used in creatures
+	target[4] = genShadow(shadowAlpha);
+	target[5] = genBorderColor(getBorderStrength(elapsedTime), border);
+	target[6] = addColors(genShadow(shadowAlpha),     genBorderColor(getBorderStrength(elapsedTime), border));
+	target[7] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border));
 }
 
 void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight)
@@ -371,8 +372,8 @@ void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter,
 		IImage::SpecialPalette SpecialPalette;
 		genSpecialPalette(SpecialPalette);
 
-		image->setSpecialPallete(SpecialPalette);
-		image->adjustPalette(shifter, 8);
+		image->setSpecialPallete(SpecialPalette, IImage::SPECIAL_PALETTE_MASK_CREATURES);
+		image->adjustPalette(shifter, IImage::SPECIAL_PALETTE_MASK_CREATURES);
 
 		canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h));
 

+ 1 - 1
client/gui/CGuiHandler.cpp

@@ -192,7 +192,7 @@ void CGuiHandler::updateTime()
 	for (auto & elem : hlp)
 	{
 		if(!vstd::contains(timeinterested,elem)) continue;
-		(elem)->onTimer(ms);
+		(elem)->tick(ms);
 	}
 }
 

+ 0 - 19
client/gui/CIntObject.cpp

@@ -29,7 +29,6 @@ CIntObject::CIntObject(int used_, Point pos_):
 	active(active_m)
 {
 	hovered = captureAllKeys = strongInterest = false;
-	toNextTick = timerDelay = 0;
 	used = used_;
 
 	recActions = defActions = GH.defActionsDef;
@@ -60,24 +59,6 @@ CIntObject::~CIntObject()
 		parent_m->removeChild(this);
 }
 
-void CIntObject::setTimer(int msToTrigger)
-{
-	if (!(active & TIME))
-		activate(TIME);
-	toNextTick = timerDelay = msToTrigger;
-	used |= TIME;
-}
-
-void CIntObject::onTimer(int timePassed)
-{
-	toNextTick -= timePassed;
-	if (toNextTick < 0)
-	{
-		toNextTick += timerDelay;
-		tick();
-	}
-}
-
 void CIntObject::show(SDL_Surface * to)
 {
 	if(defActions & UPDATE)

+ 1 - 8
client/gui/CIntObject.h

@@ -65,14 +65,8 @@ class CIntObject : public IShowActivatable //interface object
 {
 	ui16 used;//change via addUsed() or delUsed
 
-	//time handling
-	int toNextTick;
-	int timerDelay;
-
 	std::map<MouseButton, bool> currentMouseState;
 
-	void onTimer(int timePassed);
-
 	//non-const versions of fields to allow changing them in CIntObject
 	CIntObject *parent_m; //parent object
 	ui16 active_m;
@@ -129,8 +123,7 @@ public:
 	virtual void mouseMoved (const Point & cursorPosition){}
 
 	//time handling
-	void setTimer(int msToTrigger);//set timer delay and activate timer if needed.
-	virtual void tick(){}
+	virtual void tick(uint32_t msPassed){}
 
 	//mouse wheel
 	virtual void wheelScrolled(bool down, bool in){}

+ 11 - 11
client/mapView/IMapRendererObserver.h

@@ -26,27 +26,27 @@ public:
 	virtual bool hasOngoingAnimations() = 0;
 
 	/// Plays fade-in animation and adds object to map
-	virtual void onObjectFadeIn(const CGObjectInstance * obj) {}
+	virtual void onObjectFadeIn(const CGObjectInstance * obj) = 0;
 
 	/// Plays fade-out animation and removed object from map
-	virtual void onObjectFadeOut(const CGObjectInstance * obj) {}
+	virtual void onObjectFadeOut(const CGObjectInstance * obj) = 0;
 
 	/// Adds object to map instantly, with no animation
-	virtual void onObjectInstantAdd(const CGObjectInstance * obj) {}
+	virtual void onObjectInstantAdd(const CGObjectInstance * obj) = 0;
 
 	/// Removes object from map instantly, with no animation
-	virtual void onObjectInstantRemove(const CGObjectInstance * obj) {}
+	virtual void onObjectInstantRemove(const CGObjectInstance * obj) = 0;
 
 	/// Perform hero movement animation, moving hero across terrain
-	virtual void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) {}
+	virtual void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0;
 
 	/// Perform initialization of hero teleportation animation with terrain fade animation
-	virtual void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) {}
-	virtual void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) {}
+	virtual void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0;
+	virtual void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0;
 
-	virtual void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest){};
-	virtual void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest){};
+	virtual void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0;
+	virtual void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0;
 
-	virtual void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest){};
-	virtual void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest){};
+	virtual void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0;
+	virtual void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0;
 };

+ 4 - 2
client/mapView/MapView.cpp

@@ -60,14 +60,14 @@ BasicMapView::BasicMapView(const Point & offset, const Point & dimensions)
 void BasicMapView::render(Canvas & target, bool fullUpdate)
 {
 	Canvas targetClipped(target, pos);
-
-	controller->update(GH.mainFPSmng->getElapsedMilliseconds());
 	tilesCache->update(controller->getContext());
 	tilesCache->render(controller->getContext(), targetClipped, fullUpdate);
 }
 
 void BasicMapView::show(SDL_Surface * to)
 {
+	controller->update(GH.mainFPSmng->getElapsedMilliseconds());
+
 	Canvas target(to);
 	CSDL_Ext::CClipRectGuard guard(to, pos);
 	render(target, false);
@@ -75,6 +75,8 @@ void BasicMapView::show(SDL_Surface * to)
 
 void BasicMapView::showAll(SDL_Surface * to)
 {
+	controller->update(0);
+
 	Canvas target(to);
 	CSDL_Ext::CClipRectGuard guard(to, pos);
 	render(target, true);

+ 1 - 17
client/mapView/MapViewActions.cpp

@@ -24,7 +24,6 @@
 MapViewActions::MapViewActions(MapView & owner, const std::shared_ptr<MapViewModel> & model)
 	: model(model)
 	, owner(owner)
-	, curHoveredTile(-1, -1, -1)
 	, isSwiping(false)
 {
 	pos.w = model->getPixelsVisibleDimensions().x;
@@ -47,17 +46,6 @@ void MapViewActions::setContext(const std::shared_ptr<IMapRendererContext> & con
 	this->context = context;
 }
 
-void MapViewActions::activate()
-{
-	CIntObject::activate();
-}
-
-void MapViewActions::deactivate()
-{
-	CIntObject::deactivate();
-	curHoveredTile = int3(-1, -1, -1); //we lost info about hovered tile when disabling
-}
-
 void MapViewActions::clickLeft(tribool down, bool previousState)
 {
 	if(indeterminate(down))
@@ -159,11 +147,7 @@ void MapViewActions::handleHover(const Point & cursorPosition)
 		return;
 	}
 
-	if(tile != curHoveredTile)
-	{
-		curHoveredTile = tile;
-		adventureInt->onTileHovered(tile);
-	}
+	adventureInt->onTileHovered(tile);
 }
 
 void MapViewActions::hover(bool on)

+ 0 - 4
client/mapView/MapViewActions.h

@@ -23,8 +23,6 @@ class MapViewActions : public CIntObject
 	Point swipeInitialViewPos;
 	Point swipeInitialRealPos;
 
-	int3 curHoveredTile;
-
 	MapView & owner;
 	std::shared_ptr<MapViewModel> model;
 	std::shared_ptr<IMapRendererContext> context;
@@ -39,8 +37,6 @@ public:
 
 	void setContext(const std::shared_ptr<IMapRendererContext> & context);
 
-	void activate() override;
-	void deactivate() override;
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
 	void clickMiddle(tribool down, bool previousState) override;

+ 22 - 2
client/mapView/MapViewController.cpp

@@ -251,6 +251,26 @@ void MapViewController::fadeInObject(const CGObjectInstance * obj)
 
 void MapViewController::removeObject(const CGObjectInstance * obj)
 {
+	if (obj->ID == Obj::BOAT)
+	{
+		auto * boat = dynamic_cast<const CGBoat*>(obj);
+		if (boat->hero)
+		{
+			view->invalidate(context, boat->hero->id);
+			state->removeObject(boat->hero);
+		}
+	}
+
+	if (obj->ID == Obj::HERO)
+	{
+		auto * hero = dynamic_cast<const CGHeroInstance*>(obj);
+		if (hero->boat)
+		{
+			view->invalidate(context, hero->boat->id);
+			state->removeObject(hero->boat);
+		}
+	}
+
 	view->invalidate(context, obj->id);
 	state->removeObject(obj);
 }
@@ -265,7 +285,7 @@ void MapViewController::onBeforeHeroEmbark(const CGHeroInstance * obj, const int
 {
 	if(isEventVisible(obj, from, dest))
 	{
-		onObjectFadeOut(obj);
+		fadeOutObject(obj);
 		setViewCenter(obj->getSightCenter());
 	}
 	else
@@ -288,7 +308,7 @@ void MapViewController::onAfterHeroDisembark(const CGHeroInstance * obj, const i
 {
 	if(isEventVisible(obj, from, dest))
 	{
-		onObjectFadeIn(obj);
+		fadeInObject(obj);
 		setViewCenter(obj->getSightCenter());
 	}
 	addObject(obj);

+ 2 - 1
client/mapView/mapHandler.cpp

@@ -229,5 +229,6 @@ IMapObjectObserver::IMapObjectObserver()
 
 IMapObjectObserver::~IMapObjectObserver()
 {
-	CGI->mh->removeMapObserver(this);
+	if (CGI && CGI->mh)
+		CGI->mh->removeMapObserver(this);
 }

+ 4 - 3
client/render/IImage.h

@@ -40,7 +40,8 @@ enum class EImageBlitMode : uint8_t
 class IImage
 {
 public:
-	using SpecialPalette = std::array<SDL_Color, 7>;
+	using SpecialPalette = std::vector<SDL_Color>;
+	static constexpr int32_t SPECIAL_PALETTE_MASK_CREATURES = 0b11110011;
 
 	//draws image on surface "where" at position
 	virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, const Rect * src = nullptr) const = 0;
@@ -65,7 +66,7 @@ public:
 
 	//only indexed bitmaps, 16 colors maximum
 	virtual void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) = 0;
-	virtual void adjustPalette(const ColorFilter & shifter, size_t colorsToSkip) = 0;
+	virtual void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) = 0;
 	virtual void resetPalette(int colorID) = 0;
 	virtual void resetPalette() = 0;
 
@@ -73,7 +74,7 @@ public:
 	virtual void setBlitMode(EImageBlitMode mode) = 0;
 
 	//only indexed bitmaps with 7 special colors
-	virtual void setSpecialPallete(const SpecialPalette & SpecialPalette) = 0;
+	virtual void setSpecialPallete(const SpecialPalette & SpecialPalette, uint32_t colorsToSkipMask) = 0;
 
 	virtual void horizontalFlip() = 0;
 	virtual void verticalFlip() = 0;

+ 13 - 4
client/renderSDL/SDLImage.cpp

@@ -308,7 +308,7 @@ void SDLImage::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32
 	}
 }
 
-void SDLImage::adjustPalette(const ColorFilter & shifter, size_t colorsToSkip)
+void SDLImage::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
 {
 	if(originalPalette == nullptr)
 		return;
@@ -316,8 +316,11 @@ void SDLImage::adjustPalette(const ColorFilter & shifter, size_t colorsToSkip)
 	SDL_Palette* palette = surf->format->palette;
 
 	// Note: here we skip first colors in the palette that are predefined in H3 images
-	for(int i = colorsToSkip; i < palette->ncolors; i++)
+	for(int i = 0; i < palette->ncolors; i++)
 	{
+		if(i < std::numeric_limits<uint32_t>::digits && ((colorsToSkipMask >> i) & 1) == 1)
+			continue;
+
 		palette->colors[i] = shifter.shiftColor(originalPalette->colors[i]);
 	}
 }
@@ -340,11 +343,17 @@ void SDLImage::resetPalette( int colorID )
 	SDL_SetPaletteColors(surf->format->palette, originalPalette->colors + colorID, colorID, 1);
 }
 
-void SDLImage::setSpecialPallete(const IImage::SpecialPalette & SpecialPalette)
+void SDLImage::setSpecialPallete(const IImage::SpecialPalette & specialPalette, uint32_t colorsToSkipMask)
 {
 	if(surf->format->palette)
 	{
-		CSDL_Ext::setColors(surf, const_cast<SDL_Color *>(SpecialPalette.data()), 1, 7);
+		size_t last = std::min<size_t>(specialPalette.size(), surf->format->palette->ncolors);
+
+		for (size_t i = 0; i < last; ++i)
+		{
+			if(i < std::numeric_limits<uint32_t>::digits && ((colorsToSkipMask >> i) & 1) == 1)
+				surf->format->palette->colors[i] = specialPalette[i];
+		}
 	}
 }
 

+ 2 - 2
client/renderSDL/SDLImage.h

@@ -66,14 +66,14 @@ public:
 	void verticalFlip() override;
 
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
-	void adjustPalette(const ColorFilter & shifter, size_t colorsToSkip) override;
+	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
 	void resetPalette(int colorID) override;
 	void resetPalette() override;
 
 	void setAlpha(uint8_t value) override;
 	void setBlitMode(EImageBlitMode mode) override;
 
-	void setSpecialPallete(const SpecialPalette & SpecialPalette) override;
+	void setSpecialPallete(const SpecialPalette & SpecialPalette, uint32_t colorsToSkipMask) override;
 
 	friend class SDLImageLoader;
 

+ 2 - 1
client/widgets/ObjectLists.cpp

@@ -35,7 +35,8 @@ std::shared_ptr<CIntObject> CObjectList::createItem(size_t index)
 
 	item->recActions = defActions;
 	addChild(item.get());
-	item->activate();
+	if (active)
+		item->activate();
 	return item;
 }
 

+ 2 - 1
client/windows/CCastleInterface.cpp

@@ -1210,7 +1210,8 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst
 
 CCastleInterface::~CCastleInterface()
 {
-	adventureInt->onAudioResumed();
+	if (adventureInt) // may happen on exiting client with open castle interface
+		adventureInt->onAudioResumed();
 	if(LOCPLINT->castleInt == this)
 		LOCPLINT->castleInt = nullptr;
 }

+ 3 - 0
client/windows/CTradeWindow.cpp

@@ -924,6 +924,9 @@ void CMarketplaceWindow::artifactsChanged(bool Left)
 			toRemove.insert(item);
 
 	removeItems(toRemove);
+
+	// clear set to erase final instance of shared_ptr - we want to redraw screen only after it has been deleted
+	toRemove.clear();
 	redraw();
 }
 

+ 1 - 0
client/windows/CWindowObject.cpp

@@ -85,6 +85,7 @@ std::shared_ptr<CPicture> CWindowObject::createBg(std::string imageName, bool pl
 		return nullptr;
 
 	auto image = std::make_shared<CPicture>(imageName);
+	image->getSurface()->setBlitMode(EImageBlitMode::OPAQUE);
 	if(playerColored)
 		image->colorize(LOCPLINT->playerID);
 	return image;

+ 1 - 0
cmake_modules/VCMI_lib.cmake

@@ -258,6 +258,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/battle/IBattleInfoCallback.h
 		${MAIN_LIB_DIR}/battle/IBattleState.h
 		${MAIN_LIB_DIR}/battle/IUnitInfo.h
+		${MAIN_LIB_DIR}/battle/PossiblePlayerBattleAction.h
 		${MAIN_LIB_DIR}/battle/ReachabilityInfo.h
 		${MAIN_LIB_DIR}/battle/SideInBattle.h
 		${MAIN_LIB_DIR}/battle/SiegeInfo.h

+ 2 - 2
config/battleEffects.json

@@ -68,7 +68,7 @@
 				"alpha" : 0.0
 			},
 			{
-				"time" : 0.2
+				"time" : 0.5
 			},
 		],
 		"teleportFadeOut" : [
@@ -76,7 +76,7 @@
 				"time" : 0.0
 			},
 			{
-				"time" : 0.2,
+				"time" : 0.5,
 				"alpha" : 0.0
 			},
 		],

+ 2 - 2
config/schemas/settings.json

@@ -64,12 +64,12 @@
 				},
 				"language" : {
 					"type":"string",
-					"enum" : [ "chinese", "english", "german", "polish", "russian", "spanish", "ukrainian" ],
+					"enum" : [ "english", "chinese", "german", "polish", "russian", "spanish", "ukrainian" ],
 					"default" : "english"
 				},
 				"gameDataLanguage" : {
 					"type":"string",
-					"enum" : [ "auto", "chinese", "english", "german", "korean", "polish", "russian", "spanish", "ukrainian", "other_cp1250", "other_cp1251", "other_cp1252" ],
+					"enum" : [ "auto", "english", "chinese", "german", "korean", "polish", "russian", "spanish", "ukrainian", "other_cp1250", "other_cp1251", "other_cp1252" ],
 					"default" : "auto"
 				},
 				"lastSave" : {

+ 51 - 51
launcher/translation/polish.ts

@@ -145,7 +145,7 @@
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="242"/>
         <source>Contact</source>
-        <translation type="unfinished"></translation>
+        <translation>Kontakt</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="246"/>
@@ -242,7 +242,7 @@
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="590"/>
         <source>Adventure Map AI</source>
-        <translation type="unfinished"></translation>
+        <translation>AI mapy przygody</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="334"/>
@@ -261,18 +261,18 @@
         <location filename="../settingsView/csettingsview_moc.ui" line="79"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="576"/>
         <source>Artificial Intelligence</source>
-        <translation type="unfinished"></translation>
+        <translation>Sztuczna Inteligencja</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="89"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="415"/>
         <source>Mod Repositories</source>
-        <translation type="unfinished"></translation>
+        <translation>Repozytoria modów</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="368"/>
         <source>Update now</source>
-        <translation type="unfinished"></translation>
+        <translation>Zaktualizuj teraz</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="261"/>
@@ -285,37 +285,37 @@
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="215"/>
         <source>Cursor</source>
-        <translation type="unfinished"></translation>
+        <translation>Kursor</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="500"/>
         <source>Heroes III Data Language</source>
-        <translation type="unfinished"></translation>
+        <translation>Język plików Heroes III</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="552"/>
         <source>Default</source>
-        <translation type="unfinished"></translation>
+        <translation>Domyślny</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="557"/>
         <source>Hardware</source>
-        <translation type="unfinished"></translation>
+        <translation>Sprzętowy</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="562"/>
         <source>Software</source>
-        <translation type="unfinished"></translation>
+        <translation>Programowy</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="597"/>
         <source>Heroes III Translation</source>
-        <translation type="unfinished"></translation>
+        <translation>Tłumaczenie Heroes III</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="429"/>
         <source>Check on startup</source>
-        <translation type="unfinished"></translation>
+        <translation>Sprawdzaj przy uruchomieniu</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="158"/>
@@ -408,27 +408,27 @@
     <message>
         <location filename="../settingsView/csettingsview_moc.cpp" line="385"/>
         <source>Active</source>
-        <translation type="unfinished">Aktywny</translation>
+        <translation>Aktywny</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.cpp" line="390"/>
         <source>Disabled</source>
-        <translation type="unfinished"></translation>
+        <translation>Wyłączone</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.cpp" line="391"/>
         <source>Enable</source>
-        <translation type="unfinished">Włącz</translation>
+        <translation>Włącz</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.cpp" line="396"/>
         <source>Not Installed</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie zainstalowano</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.cpp" line="397"/>
         <source>Install</source>
-        <translation type="unfinished">Zainstaluj</translation>
+        <translation>Zainstaluj</translation>
     </message>
 </context>
 <context>
@@ -436,134 +436,134 @@
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="28"/>
         <source>Language</source>
-        <translation type="unfinished"></translation>
+        <translation>Język</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="53"/>
         <source>Heroes III Data</source>
-        <translation type="unfinished"></translation>
+        <translation>Pliki Heroes III</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="78"/>
         <source>Mods Preset</source>
-        <translation type="unfinished"></translation>
+        <translation>Zestaw modów</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="384"/>
         <source>Your Heroes III data files have been successfully found.</source>
-        <translation type="unfinished"></translation>
+        <translation>Twoje pliki Heroes III zostały pomyślnie znalezione.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="552"/>
         <source>Optionally, you can install additional mods either now or at any point later:</source>
-        <translation type="unfinished"></translation>
+        <translation>Opcjonalnie możesz zainstalować dodatkowe modyfikacje teraz lub później:</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="616"/>
         <source>Install support for playing Heroes III in resolutions other than 800x600.</source>
-        <translation type="unfinished"></translation>
+        <translation>Zapinstaluj wsparcie dla grania w Heroes III w rozdzielczości innej niż 800x600.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="645"/>
         <source>Install compatible version of addon Horn of the Abyss: fan-made Heroes III expansion, ported by VCMI team</source>
-        <translation type="unfinished"></translation>
+        <translation>Zainstaluj kompatybilną wersję fanowskiego dodatku Horn of the Abyss przeportowaną przez zespół VCMI</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="674"/>
         <source>Install compatible version of addon &quot;In The Wake of Gods&quot;: fan-made Heroes III expansion</source>
-        <translation type="unfinished"></translation>
+        <translation>Zainstaluj kompatybilną wersję fanowskiego dodatku &quot;In The Wake Of Gods&quot;</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="721"/>
         <source>Finish</source>
-        <translation type="unfinished"></translation>
+        <translation>Zakończ</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="111"/>
         <source>Step %v out of %m</source>
-        <translation type="unfinished"></translation>
+        <translation>Krok %v z %m</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="143"/>
         <source>Choose your language</source>
-        <translation type="unfinished"></translation>
+        <translation>Wybierz język</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="150"/>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="493"/>
         <source>Next</source>
-        <translation type="unfinished"></translation>
+        <translation>Dalej</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="215"/>
         <source>Find Heroes III data files</source>
-        <translation type="unfinished"></translation>
+        <translation>Znajdź pliki Heroes III</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="246"/>
         <source>Open help in browser</source>
-        <translation type="unfinished"></translation>
+        <translation>Otwórz pomoc w przeglądarce</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="259"/>
         <source>Search again</source>
-        <translation type="unfinished"></translation>
+        <translation>Szukaj ponownie</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="304"/>
         <source>If you don&apos;t have installed Heroes III copy, it is possible to use our automatic installation tool &apos;vcmibuilder&apos; to extract data from GoG.com installer. Visit our wiki for detailed instructions.</source>
-        <translation type="unfinished"></translation>
+        <translation>Jeśli nie masz zainstalowanej kopii Heroes III istnieje możliwość użycia naszego automatycznego narzędzia instalacyjnego &apos;vcmibuilder&apos; by wyodrębnić dane z instalatora GoG.com. Odwiedź nasze wiki po szczegółowe instrukcje.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="320"/>
         <source>VCMI requires Heroes III data files in one of the locations listed above. Please copy Heroes III data in one of these directories.</source>
-        <translation type="unfinished"></translation>
+        <translation>VCMI wymaga plików Heroes III w jednej z wymienionych wyżej lokalizacji. Proszę, skopiuj pliki Heroes III do jednego z tych katalogów.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="342"/>
         <source>Heroes III data files</source>
-        <translation type="unfinished"></translation>
+        <translation>Pliki Heroes III</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="355"/>
         <source>Alternatively, you can select directory with installed Heroes III data and VCMI will copy exisiting data automatically.</source>
-        <translation type="unfinished"></translation>
+        <translation>Możesz też wybrać folder z zainstalowanym Heroes III i VCMI automatycznie skopiuje istniejące dane.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="371"/>
         <source>Copy existing data</source>
-        <translation type="unfinished"></translation>
+        <translation>Skopiuj istniejące dane</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="414"/>
         <source>Your Heroes III language has been successfully detected.</source>
-        <translation type="unfinished"></translation>
+        <translation>Twój język Heroes III został pomyślnie wykryty.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="424"/>
         <source>Automatic detection of language failed. Please select language of your Heroes III copy</source>
-        <translation type="unfinished"></translation>
+        <translation>Automatyczna detekcja języka nie powiodła się. Proszę wybrać język twojego Heroes III</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="443"/>
         <source>Heroes III language</source>
-        <translation type="unfinished"></translation>
+        <translation>Język Heroes III</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="486"/>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="714"/>
         <source>Back</source>
-        <translation type="unfinished"></translation>
+        <translation>Wstecz</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="524"/>
         <source>Install VCMI Mod Preset</source>
-        <translation type="unfinished"></translation>
+        <translation>Zainstaluj zestaw modyfikacji</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="584"/>
         <source>Install translation of Heroes III to your language</source>
-        <translation type="unfinished"></translation>
+        <translation>Zainstaluj tłumaczenie Heroes III dla twojego języka</translation>
     </message>
 </context>
 <context>
@@ -663,12 +663,12 @@
     <message>
         <location filename="../lobby/lobby_moc.ui" line="76"/>
         <source>People in lobby</source>
-        <translation type="unfinished"></translation>
+        <translation>Ludzie w lobby</translation>
     </message>
     <message>
         <location filename="../lobby/lobby_moc.ui" line="114"/>
         <source>Lobby chat</source>
-        <translation type="unfinished"></translation>
+        <translation>Czat lobby</translation>
     </message>
     <message>
         <location filename="../lobby/lobby_moc.ui" line="194"/>
@@ -683,17 +683,17 @@
     <message>
         <location filename="../lobby/lobby_moc.ui" line="274"/>
         <source>Resolve</source>
-        <translation type="unfinished"></translation>
+        <translation>Rozwiąż</translation>
     </message>
     <message>
         <location filename="../lobby/lobby_moc.ui" line="286"/>
         <source>New game</source>
-        <translation type="unfinished"></translation>
+        <translation>Nowa gra</translation>
     </message>
     <message>
         <location filename="../lobby/lobby_moc.ui" line="293"/>
         <source>Load game</source>
-        <translation type="unfinished"></translation>
+        <translation>Wczytaj grę</translation>
     </message>
     <message>
         <location filename="../lobby/lobby_moc.ui" line="149"/>
@@ -733,12 +733,12 @@
     <message>
         <location filename="../lobby/lobby_moc.cpp" line="369"/>
         <source>Disconnect</source>
-        <translation type="unfinished"></translation>
+        <translation>Rozłącz</translation>
     </message>
     <message>
         <location filename="../lobby/lobby_moc.cpp" line="461"/>
         <source>No issues detected</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie znaleziono problemów</translation>
     </message>
 </context>
 <context>
@@ -779,7 +779,7 @@
     <message>
         <location filename="../mainwindow_moc.ui" line="226"/>
         <source>Map Editor</source>
-        <translation type="unfinished"></translation>
+        <translation>Edytor map</translation>
     </message>
     <message>
         <location filename="../mainwindow_moc.ui" line="279"/>

+ 1 - 1
lib/CModHandler.cpp

@@ -684,7 +684,7 @@ void CModInfo::loadLocalData(const JsonNode & data)
 {
 	bool validated = false;
 	implicitlyEnabled = true;
-	explicitlyEnabled = true;
+	explicitlyEnabled = !config["keepDisabled"].Bool();
 	checksum = 0;
 	if (data.getType() == JsonNode::JsonType::DATA_BOOL)
 	{

+ 2 - 2
lib/GameConstants.h

@@ -1120,6 +1120,7 @@ public:
 		LICHES = 64,
 		BONE_DRAGON = 68,
 		TROGLODYTES = 70,
+		MEDUSA = 76,
 		HYDRA = 110,
 		CHAOS_HYDRA = 111,
 		AIR_ELEMENTAL = 112,
@@ -1245,7 +1246,7 @@ class ObstacleInfo;
 class Obstacle : public BaseForID<Obstacle, si32>
 {
 	INSTID_LIKE_CLASS_COMMON(Obstacle, si32)
-	
+
 	DLL_LINKAGE const ObstacleInfo * getInfo() const;
 	DLL_LINKAGE operator std::string() const;
 	DLL_LINKAGE static Obstacle fromString(const std::string & identifier);
@@ -1295,7 +1296,6 @@ enum class EHealPower : ui8
 // Typedef declarations
 typedef ui8 TFaction;
 typedef si64 TExpType;
-typedef std::pair<si64, si64> TDmgRange;
 typedef si32 TBonusSubtype;
 typedef si32 TQuantity;
 

+ 4 - 4
lib/battle/BattleInfo.cpp

@@ -663,14 +663,14 @@ const IBonusBearer * BattleInfo::asBearer() const
 	return this;
 }
 
-int64_t BattleInfo::getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const
+int64_t BattleInfo::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const
 {
-	if(damage.first != damage.second)
+	if(damage.min != damage.max)
 	{
 		int64_t sum = 0;
 
 		auto howManyToAv = std::min<int32_t>(10, attackerCount);
-		auto rangeGen = rng.getInt64Range(damage.first, damage.second);
+		auto rangeGen = rng.getInt64Range(damage.min, damage.max);
 
 		for(int32_t g = 0; g < howManyToAv; ++g)
 			sum += rangeGen();
@@ -679,7 +679,7 @@ int64_t BattleInfo::getActualDamage(const TDmgRange & damage, int32_t attackerCo
 	}
 	else
 	{
-		return damage.first;
+		return damage.min;
 	}
 }
 

+ 1 - 1
lib/battle/BattleInfo.h

@@ -97,7 +97,7 @@ public:
 
 	uint32_t nextUnitId() const override;
 
-	int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
+	int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
 
 	//////////////////////////////////////////////////////////////////////////
 	// IBattleState

+ 48 - 26
lib/battle/CBattleInfoCallback.cpp

@@ -15,6 +15,7 @@
 #include "../CStack.h"
 #include "BattleInfo.h"
 #include "DamageCalculator.h"
+#include "PossiblePlayerBattleAction.h"
 #include "../NetPacks.h"
 #include "../spells/CSpellHandler.h"
 #include "../mapObjects/CGTownInstance.h"
@@ -218,11 +219,14 @@ std::vector<PossiblePlayerBattleAction> CBattleInfoCallback::getClientActionsFor
 	{
 		if(stack->canCast()) //TODO: check for battlefield effects that prevent casting?
 		{
-			if(stack->hasBonusOfType(Bonus::SPELLCASTER) && data.creatureSpellToCast != -1)
+			if(stack->hasBonusOfType(Bonus::SPELLCASTER))
 			{
-				const CSpell *spell = SpellID(data.creatureSpellToCast).toSpell();
-				PossiblePlayerBattleAction act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE);
-				allowedActionList.push_back(act);
+				for (auto const & spellID : data.creatureSpellsToCast)
+				{
+					const CSpell *spell = spellID.toSpell();
+					PossiblePlayerBattleAction act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE);
+					allowedActionList.push_back(act);
+				}
 			}
 			if(stack->hasBonusOfType(Bonus::RANDOM_SPELLCASTER))
 				allowedActionList.push_back(PossiblePlayerBattleAction::RANDOM_GENIE_SPELL);
@@ -251,7 +255,7 @@ std::vector<PossiblePlayerBattleAction> CBattleInfoCallback::getClientActionsFor
 PossiblePlayerBattleAction CBattleInfoCallback::getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const
 {
 	RETURN_IF_NOT_BATTLE(PossiblePlayerBattleAction::INVALID);
-	PossiblePlayerBattleAction spellSelMode = PossiblePlayerBattleAction::ANY_LOCATION;
+	auto spellSelMode = PossiblePlayerBattleAction::ANY_LOCATION;
 
 	const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode);
 
@@ -264,7 +268,7 @@ PossiblePlayerBattleAction CBattleInfoCallback::getCasterAction(const CSpell * s
 	else if(ti.type == spells::AimType::OBSTACLE)
 		spellSelMode = PossiblePlayerBattleAction::OBSTACLE;
 
-	return spellSelMode;
+	return PossiblePlayerBattleAction(spellSelMode, spell->id);
 }
 
 std::set<BattleHex> CBattleInfoCallback::battleGetAttackedHexes(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos) const
@@ -715,57 +719,64 @@ bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHe
 	return false;
 }
 
-TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const
+DamageEstimation CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const
 {
 	DamageCalculator calculator(*this, info);
 
 	return calculator.calculateDmgRange();
 }
 
-TDmgRange CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, TDmgRange * retaliationDmg) const
+DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg) const
 {
-	RETURN_IF_NOT_BATTLE(std::make_pair(0, 0));
+	RETURN_IF_NOT_BATTLE({});
 	auto reachability = battleGetDistances(attacker, attacker->getPosition());
 	int movementDistance = reachability[attackerPosition];
 	return battleEstimateDamage(attacker, defender, movementDistance, retaliationDmg);
 }
 
-TDmgRange CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, TDmgRange * retaliationDmg) const
+DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg) const
 {
-	RETURN_IF_NOT_BATTLE(std::make_pair(0, 0));
+	RETURN_IF_NOT_BATTLE({});
 	const bool shooting = battleCanShoot(attacker, defender->getPosition());
 	const BattleAttackInfo bai(attacker, defender, movementDistance, shooting);
 	return battleEstimateDamage(bai, retaliationDmg);
 }
 
-TDmgRange CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo & bai, TDmgRange * retaliationDmg) const
+DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg) const
 {
-	RETURN_IF_NOT_BATTLE(std::make_pair(0, 0));
+	RETURN_IF_NOT_BATTLE({});
 
-	TDmgRange ret = calculateDmgRange(bai);
+	DamageEstimation ret = calculateDmgRange(bai);
 
 	if(retaliationDmg)
 	{
 		if(bai.shooting)
 		{
 			//FIXME: handle RANGED_RETALIATION
-			retaliationDmg->first = retaliationDmg->second = 0;
+			*retaliationDmg = DamageEstimation();
 		}
 		else
 		{
 			//TODO: rewrite using boost::numeric::interval
 			//TODO: rewire once more using interval-based fuzzy arithmetic
 
-			int64_t TDmgRange::* pairElems[] = {&TDmgRange::first, &TDmgRange::second};
-			for (int i=0; i<2; ++i)
+			auto const & estimateRetaliation = [&]( int64_t damage)
 			{
 				auto retaliationAttack = bai.reverse();
-				int64_t dmg = ret.*pairElems[i];
 				auto state = retaliationAttack.attacker->acquireState();
-				state->damage(dmg);
+				state->damage(damage);
 				retaliationAttack.attacker = state.get();
-				retaliationDmg->*pairElems[!i] = calculateDmgRange(retaliationAttack).*pairElems[!i];
-			}
+				return calculateDmgRange(retaliationAttack);
+			};
+
+			DamageEstimation retaliationMin = estimateRetaliation(ret.damage.min);
+			DamageEstimation retaliationMax = estimateRetaliation(ret.damage.min);
+
+			retaliationDmg->damage.min = std::min(retaliationMin.damage.min, retaliationMax.damage.min);
+			retaliationDmg->damage.max = std::max(retaliationMin.damage.max, retaliationMax.damage.max);
+
+			retaliationDmg->kills.min = std::min(retaliationMin.kills.min, retaliationMax.kills.min);
+			retaliationDmg->kills.max = std::max(retaliationMin.kills.max, retaliationMax.kills.max);
 		}
 	}
 
@@ -993,12 +1004,19 @@ std::set<BattleHex> CBattleInfoCallback::getStoppers(BattlePerspective::BattlePe
 
 	for(auto &oi : battleGetAllObstacles(whichSidePerspective))
 	{
-		if(battleIsObstacleVisibleForSide(*oi, whichSidePerspective))
+		if(!battleIsObstacleVisibleForSide(*oi, whichSidePerspective))
+			continue;
+
+		for(const auto & hex : oi->getStoppingTile())
 		{
-			range::copy(oi->getStoppingTile(), vstd::set_inserter(ret));
+			if(hex == ESiegeHex::GATE_BRIDGE && oi->obstacleType == CObstacleInstance::MOAT)
+			{
+				if(battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED)
+					continue; // this tile is disabled by drawbridge on top of it
+			}
+			ret.insert(hex);
 		}
 	}
-
 	return ret;
 }
 
@@ -1646,12 +1664,16 @@ SpellID CBattleInfoCallback::getRandomCastedSpell(CRandomGenerator & rand,const
 	int totalWeight = 0;
 	for(const auto & b : *bl)
 	{
-		totalWeight += std::max(b->additionalInfo[0], 1); //minimal chance to cast is 1
+		totalWeight += std::max(b->additionalInfo[0], 0); //spells with 0 weight are non-random, exclude them
 	}
+
+	if (totalWeight == 0)
+		return SpellID::NONE;
+
 	int randomPos = rand.nextInt(totalWeight - 1);
 	for(const auto & b : *bl)
 	{
-		randomPos -= std::max(b->additionalInfo[0], 1);
+		randomPos -= std::max(b->additionalInfo[0], 0);
 		if(randomPos < 0)
 		{
 			return SpellID(b->subtype);

+ 6 - 31
lib/battle/CBattleInfoCallback.h

@@ -24,6 +24,7 @@ class CSpell;
 struct CObstacleInstance;
 class IBonusBearer;
 class CRandomGenerator;
+class PossiblePlayerBattleAction;
 
 namespace spells
 {
@@ -42,35 +43,9 @@ struct DLL_LINKAGE AttackableTiles
 	}
 };
 
-enum class PossiblePlayerBattleAction // actions performed at l-click
-{
-	INVALID = -1,
-	CREATURE_INFO,
-	HERO_INFO,
-	MOVE_TACTICS,
-	CHOOSE_TACTICS_STACK,
-
-	MOVE_STACK,
-	ATTACK,
-	WALK_AND_ATTACK,
-	ATTACK_AND_RETURN,
-	SHOOT,
-	CATAPULT,
-	HEAL,
-
-	NO_LOCATION,          // massive spells that affect every possible target, automatic casts
-	ANY_LOCATION,
-	OBSTACLE,
-	TELEPORT,
-	SACRIFICE,
-	RANDOM_GENIE_SPELL,   // random spell on a friendly creature
-	FREE_LOCATION,        // used with Force Field and Fire Wall - all tiles affected by spell must be free
-	AIMED_SPELL_CREATURE, // spell targeted at creature
-};
-
 struct DLL_LINKAGE BattleClientInterfaceData
 {
-	si32 creatureSpellToCast;
+	std::vector<SpellID> creatureSpellsToCast;
 	ui8 tacticsMode;
 };
 
@@ -117,14 +92,14 @@ public:
 	bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack
 	std::set<const battle::Unit *> battleAdjacentUnits(const battle::Unit * unit) const;
 
-	TDmgRange calculateDmgRange(const BattleAttackInfo & info) const; //charge - number of hexes travelled before attack (for champion's jousting); returns pair <min dmg, max dmg>
+	DamageEstimation calculateDmgRange(const BattleAttackInfo & info) const;
 
 	/// estimates damage dealt by attacker to defender;
 	/// only non-random bonuses are considered in estimation
 	/// returns pair <min dmg, max dmg>
-	TDmgRange battleEstimateDamage(const BattleAttackInfo & bai, TDmgRange * retaliationDmg = nullptr) const;
-	TDmgRange battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, TDmgRange * retaliationDmg = nullptr) const;
-	TDmgRange battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, TDmgRange * retaliationDmg = nullptr) const;
+	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;
 
 	bool battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const;
 	bool battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const;

+ 3 - 0
lib/battle/CUnitState.h

@@ -112,7 +112,10 @@ public:
 	int32_t getFirstHPleft() const;
 	int32_t getResurrected() const;
 
+	/// returns total remaining health
 	int64_t available() const;
+
+	/// returns total initial health
 	int64_t total() const;
 
 	void takeResurrected();

+ 45 - 20
lib/battle/DamageCalculator.cpp

@@ -20,10 +20,10 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-TDmgRange DamageCalculator::getBaseDamageSingle() const
+DamageRange DamageCalculator::getBaseDamageSingle() const
 {
-	double minDmg = 0.0;
-	double maxDmg = 0.0;
+	int64_t minDmg = 0.0;
+	int64_t maxDmg = 0.0;
 
 	minDmg = info.attacker->getMinDamage(info.shooting);
 	maxDmg = info.attacker->getMaxDamage(info.shooting);
@@ -63,7 +63,7 @@ TDmgRange DamageCalculator::getBaseDamageSingle() const
 	return { minDmg, maxDmg };
 }
 
-TDmgRange DamageCalculator::getBaseDamageBlessCurse() const
+DamageRange DamageCalculator::getBaseDamageBlessCurse() const
 {
 	const std::string cachingStrForcedMinDamage = "type_ALWAYS_MINIMUM_DAMAGE";
 	static const auto selectorForcedMinDamage = Selector::type()(Bonus::ALWAYS_MINIMUM_DAMAGE);
@@ -76,10 +76,10 @@ TDmgRange DamageCalculator::getBaseDamageBlessCurse() const
 
 	int curseBlessAdditiveModifier = blessEffects->totalValue() - curseEffects->totalValue();
 
-	TDmgRange baseDamage = getBaseDamageSingle();
-	TDmgRange modifiedDamage = {
-		std::max(static_cast<int64_t>(1), baseDamage.first + curseBlessAdditiveModifier),
-		std::max(static_cast<int64_t>(1), baseDamage.second + curseBlessAdditiveModifier)
+	DamageRange baseDamage = getBaseDamageSingle();
+	DamageRange modifiedDamage = {
+		std::max(static_cast<int64_t>(1), baseDamage.min + curseBlessAdditiveModifier),
+		std::max(static_cast<int64_t>(1), baseDamage.max + curseBlessAdditiveModifier)
 	};
 
 	if(curseEffects->size() && blessEffects->size() )
@@ -91,29 +91,29 @@ TDmgRange DamageCalculator::getBaseDamageBlessCurse() const
 	if(curseEffects->size())
 	{
 		return {
-			modifiedDamage.first,
-			modifiedDamage.first
+			modifiedDamage.min,
+			modifiedDamage.min
 		};
 	}
 
 	if(blessEffects->size())
 	{
 		return {
-			modifiedDamage.second,
-			modifiedDamage.second
+			modifiedDamage.max,
+			modifiedDamage.max
 		};
 	}
 
 	return modifiedDamage;
 }
 
-TDmgRange DamageCalculator::getBaseDamageStack() const
+DamageRange DamageCalculator::getBaseDamageStack() const
 {
 	auto stackSize = info.attacker->getCount();
 	auto baseDamage = getBaseDamageBlessCurse();
 	return {
-		baseDamage.first * stackSize,
-		baseDamage.second * stackSize
+		baseDamage.min * stackSize,
+		baseDamage.max * stackSize
 	};
 }
 
@@ -450,6 +450,25 @@ std::vector<double> DamageCalculator::getDefenseFactors() const
 	};
 }
 
+DamageRange DamageCalculator::getCasualties(const DamageRange & damageDealt) const
+{
+	return {
+		getCasualties(damageDealt.min),
+		getCasualties(damageDealt.max),
+	};
+}
+
+int64_t DamageCalculator::getCasualties(int64_t damageDealt) const
+{
+	if (damageDealt < info.defender->getFirstHPleft())
+		return 0;
+
+	int64_t damageLeft = damageDealt - info.defender->getFirstHPleft();
+	int64_t killsLeft = damageLeft / info.defender->MaxHealth();
+
+	return 1 + killsLeft;
+}
+
 int DamageCalculator::battleBonusValue(const IBonusBearer * bearer, const CSelector & selector) const
 {
 	auto noLimit = Selector::effectRange()(Bonus::NO_LIMIT);
@@ -461,9 +480,9 @@ int DamageCalculator::battleBonusValue(const IBonusBearer * bearer, const CSelec
 	return bearer->getBonuses(selector, noLimit.Or(limitMatches))->totalValue();
 };
 
-TDmgRange DamageCalculator::calculateDmgRange() const
+DamageEstimation DamageCalculator::calculateDmgRange() const
 {
-	TDmgRange result = getBaseDamageStack();
+	DamageRange damageBase = getBaseDamageStack();
 
 	auto attackFactors = getAttackFactors();
 	auto defenseFactors = getDefenseFactors();
@@ -485,10 +504,16 @@ TDmgRange DamageCalculator::calculateDmgRange() const
 
 	double resultingFactor = std::min(8.0, attackFactorTotal) * std::max( 0.01, defenseFactorTotal);
 
-	return {
-		std::max( 1.0, std::floor(result.first * resultingFactor)),
-		std::max( 1.0, std::floor(result.second * resultingFactor))
+	info.defender->getTotalHealth();
+
+	DamageRange damageDealt {
+		std::max<int64_t>( 1.0, std::floor(damageBase.min * resultingFactor)),
+		std::max<int64_t>( 1.0, std::floor(damageBase.max * resultingFactor))
 	};
+
+	DamageRange killsDealt = getCasualties(damageDealt);
+
+	return DamageEstimation{damageDealt, killsDealt};
 }
 
 VCMI_LIB_NAMESPACE_END

+ 9 - 4
lib/battle/DamageCalculator.h

@@ -18,6 +18,8 @@ class CBattleInfoCallback;
 class IBonusBearer;
 class CSelector;
 struct BattleAttackInfo;
+struct DamageRange;
+struct DamageEstimation;
 
 class DLL_LINKAGE DamageCalculator
 {
@@ -26,9 +28,12 @@ class DLL_LINKAGE DamageCalculator
 
 	int battleBonusValue(const IBonusBearer * bearer, const CSelector & selector) const;
 
-	TDmgRange getBaseDamageSingle() const;
-	TDmgRange getBaseDamageBlessCurse() const;
-	TDmgRange getBaseDamageStack() const;
+	DamageRange getCasualties(const DamageRange & damageDealt) const;
+	int64_t getCasualties(int64_t damageDealt) const;
+
+	DamageRange getBaseDamageSingle() const;
+	DamageRange getBaseDamageBlessCurse() const;
+	DamageRange getBaseDamageStack() const;
 
 	int getActorAttackBase() const;
 	int getActorAttackEffective() const;
@@ -66,7 +71,7 @@ public:
 		info(info)
 	{}
 
-	TDmgRange calculateDmgRange() const;
+	DamageEstimation calculateDmgRange() const;
 };
 
 VCMI_LIB_NAMESPACE_END

+ 12 - 0
lib/battle/IBattleInfoCallback.h

@@ -26,6 +26,18 @@ namespace battle
 	using UnitFilter = std::function<bool(const Unit *)>;
 }
 
+struct DamageRange
+{
+	int64_t min = 0;
+	int64_t max = 0;
+};
+
+struct DamageEstimation
+{
+	DamageRange damage;
+	DamageRange kills;
+};
+
 #if SCRIPTING_ENABLED
 namespace scripting
 {

+ 1 - 1
lib/battle/IBattleState.h

@@ -66,7 +66,7 @@ public:
 
 	virtual uint32_t nextUnitId() const = 0;
 
-	virtual int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0;
+	virtual int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0;
 };
 
 class DLL_LINKAGE IBattleState : public IBattleInfo

+ 78 - 0
lib/battle/PossiblePlayerBattleAction.h

@@ -0,0 +1,78 @@
+/*
+ * CBattleInfoCallback.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "../GameConstants.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class PossiblePlayerBattleAction // actions performed at l-click
+{
+public:
+	enum Actions {
+		INVALID = -1,
+		CREATURE_INFO,
+		HERO_INFO,
+		MOVE_TACTICS,
+		CHOOSE_TACTICS_STACK,
+
+		MOVE_STACK,
+		ATTACK,
+		WALK_AND_ATTACK,
+		ATTACK_AND_RETURN,
+		SHOOT,
+		CATAPULT,
+		HEAL,
+
+		RANDOM_GENIE_SPELL,   // random spell on a friendly creature
+
+		NO_LOCATION,          // massive spells that affect every possible target, automatic casts
+		ANY_LOCATION,
+		OBSTACLE,
+		TELEPORT,
+		SACRIFICE,
+		FREE_LOCATION,        // used with Force Field and Fire Wall - all tiles affected by spell must be free
+		AIMED_SPELL_CREATURE, // spell targeted at creature
+	};
+
+private:
+	Actions action;
+	SpellID spellToCast;
+
+public:
+	bool spellcast() const
+	{
+		return action == ANY_LOCATION || action == NO_LOCATION || action == OBSTACLE || action == TELEPORT ||
+			   action == SACRIFICE || action == FREE_LOCATION || action == AIMED_SPELL_CREATURE;
+	}
+
+	Actions get() const
+	{
+		return action;
+	}
+
+	SpellID spell() const
+	{
+		return spellToCast;
+	}
+
+	PossiblePlayerBattleAction(Actions action, SpellID spellToCast = SpellID::NONE):
+		action(static_cast<Actions>(action)),
+		spellToCast(spellToCast)
+	{
+		assert((spellToCast != SpellID::NONE) == spellcast());
+	}
+
+	bool operator == (const PossiblePlayerBattleAction & other) const
+	{
+		return action == other.action && spellToCast == other.spellToCast;
+	}
+};
+
+VCMI_LIB_NAMESPACE_END

+ 9 - 0
lib/battle/Unit.h

@@ -70,10 +70,19 @@ public:
 	virtual bool canShoot() const = 0;
 	virtual bool isShooter() const = 0;
 
+	/// returns initial size of this unit
 	virtual int32_t getCount() const = 0;
+
+	/// returns remaining health of first unit
 	virtual int32_t getFirstHPleft() const = 0;
+
+	/// returns total amount of killed in this unit
 	virtual int32_t getKilled() const = 0;
+
+	/// returns total health that unit still has
 	virtual int64_t getAvailableHealth() const = 0;
+
+	/// returns total health that unit had initially
 	virtual int64_t getTotalHealth() const = 0;
 
 	virtual int getTotalAttacks(bool ranged) const = 0;

+ 3 - 3
lib/mapObjects/CGTownInstance.cpp

@@ -12,7 +12,7 @@
 #include "CGTownInstance.h"
 #include "CObjectClassesHandler.h"
 #include "../spells/CSpellHandler.h"
-
+#include "../battle/IBattleInfoCallback.h"
 #include "../NetPacks.h"
 #include "../CConfigHandler.h"
 #include "../CGeneralTextHandler.h"
@@ -807,7 +807,7 @@ void CGTownInstance::addTownBonuses()
 	}
 }
 
-TDmgRange CGTownInstance::getTowerDamageRange() const
+DamageRange CGTownInstance::getTowerDamageRange() const
 {
 	assert(hasBuilt(BuildingID::CASTLE));
 
@@ -825,7 +825,7 @@ TDmgRange CGTownInstance::getTowerDamageRange() const
 	};
 }
 
-TDmgRange CGTownInstance::getKeepDamageRange() const
+DamageRange CGTownInstance::getKeepDamageRange() const
 {
 	assert(hasBuilt(BuildingID::CITADEL));
 

+ 3 - 2
lib/mapObjects/CGTownInstance.h

@@ -20,6 +20,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class CCastleEvent;
 class CGTownInstance;
 class CGDwelling;
+struct DamageRange;
 
 class DLL_LINKAGE CSpecObjInfo
 {
@@ -332,10 +333,10 @@ public:
 	void deleteTownBonus(BuildingID::EBuildingID bid);
 
 	/// Returns damage range for secondary towers of this town
-	TDmgRange getTowerDamageRange() const;
+	DamageRange getTowerDamageRange() const;
 
 	/// Returns damage range for central tower(keep) of this town
-	TDmgRange getKeepDamageRange() const;
+	DamageRange getKeepDamageRange() const;
 
 	const CTown * getTown() const ;
 

+ 34 - 3
lib/mapObjects/CObjectClassesHandler.cpp

@@ -399,12 +399,43 @@ void CObjectClassesHandler::afterLoadFinalization()
 		}
 	}
 
+	generateExtraMonolithsForRMG();
+}
+
+void CObjectClassesHandler::generateExtraMonolithsForRMG()
+{
 	//duplicate existing two-way portals to make reserve for RMG
 	auto& portalVec = objects[Obj::MONOLITH_TWO_WAY]->objects;
-	size_t portalCount = portalVec.size();
+	//FIXME: Monoliths  in this vector can be already not useful for every terrain
+	const size_t portalCount = portalVec.size();
+
+	//Invalid portals will be skipped and portalVec size stays unchanged
+	for (size_t i = portalCount; portalVec.size() < 100; ++i)
+	{
+		auto index = static_cast<si32>(i % portalCount);
+		auto portal = portalVec[index];
+		auto templates = portal->getTemplates();
+		if (templates.empty() || !templates[0]->canBePlacedAtAnyTerrain())
+		{
+			continue; //Do not clone HoTA water-only portals or any others we can't use
+		}
 
-	for (size_t i = portalCount; i < 100; ++i)
-		portalVec.push_back(portalVec[static_cast<si32>(i % portalCount)]);
+		//deep copy of noncopyable object :?
+		auto newPortal = std::make_shared<CDefaultObjectTypeHandler<CGMonolith>>();
+		newPortal->rmgInfo = portal->getRMGInfo();
+		newPortal->base = portal->base; //not needed?
+		newPortal->templates = portal->getTemplates();
+		newPortal->sounds = portal->getSounds();
+		newPortal->aiValue = portal->getAiValue();
+		newPortal->battlefield = portal->battlefield; //getter is not initialized at this point
+		newPortal->modScope = portal->modScope; //private
+		newPortal->typeName = portal->getTypeName(); 
+		newPortal->subTypeName = std::string("monolith") + std::to_string(portalVec.size());
+		newPortal->type = portal->getIndex();
+
+		newPortal->subtype = portalVec.size(); //indexes must be unique, they are returned as a set
+		portalVec.push_back(newPortal);
+	}
 }
 
 std::string CObjectClassesHandler::getObjectName(si32 type, si32 subtype) const

+ 2 - 0
lib/mapObjects/CObjectClassesHandler.h

@@ -292,6 +292,8 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase
 
 	ObjectClass * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index);
 
+	void generateExtraMonolithsForRMG();
+
 public:
 	CObjectClassesHandler();
 	~CObjectClassesHandler();

+ 5 - 0
lib/mapObjects/ObjectTemplate.h

@@ -102,6 +102,11 @@ public:
 		return visitDir & 2;
 	};
 
+	inline bool canBePlacedAtAnyTerrain() const
+	{
+		return anyTerrain;
+	}; 
+
 	// Checks if object can be placed on specific terrain
 	bool canBePlacedAt(TerrainId terrain) const;
 

+ 18 - 4
lib/rmg/CMapGenerator.cpp

@@ -367,10 +367,24 @@ void CMapGenerator::addHeaderInfo()
 
 int CMapGenerator::getNextMonlithIndex()
 {
-	if (monolithIndex >= VLC->objtypeh->knownSubObjects(Obj::MONOLITH_TWO_WAY).size())
-		throw rmgException(boost::to_string(boost::format("There is no Monolith Two Way with index %d available!") % monolithIndex));
-	else
-		return monolithIndex++;
+	while (true)
+	{
+		if (monolithIndex >= VLC->objtypeh->knownSubObjects(Obj::MONOLITH_TWO_WAY).size())
+			throw rmgException(boost::to_string(boost::format("There is no Monolith Two Way with index %d available!") % monolithIndex));
+		else
+		{
+			//Skip modded Monoliths which can't beplaced on every terrain
+			auto templates = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, monolithIndex)->getTemplates();
+			if (templates.empty() || !templates[0]->canBePlacedAtAnyTerrain())
+			{
+				monolithIndex++;
+			}
+			else
+			{
+				return monolithIndex++;
+			}
+		}
+	}
 }
 
 int CMapGenerator::getPrisonsRemaning() const

+ 20 - 11
lib/rmg/ConnectionsPlacer.cpp

@@ -100,15 +100,20 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 			borderPos = *RandomGeneratorUtil::nextItem(directConnectionIterator->second, generator.rand);
 			guardPos = zone.areaPossible().nearest(borderPos);
 			assert(borderPos != guardPos);
-			
-			auto safetyGap = rmg::Area({guardPos});
-			safetyGap.unite(safetyGap.getBorderOutside());
-			safetyGap.intersect(zone.areaPossible());
-			if(!safetyGap.empty())
+
+			float dist = map.getTile(guardPos).getNearestObjectDistance();
+			if (dist >= 3) //Don't place guards at adjacent tiles
 			{
-				safetyGap.intersect(otherZone->areaPossible());
-				if(safetyGap.empty())
-					break; //successfull position
+
+				auto safetyGap = rmg::Area({ guardPos });
+				safetyGap.unite(safetyGap.getBorderOutside());
+				safetyGap.intersect(zone.areaPossible());
+				if (!safetyGap.empty())
+				{
+					safetyGap.intersect(otherZone->areaPossible());
+					if (safetyGap.empty())
+						break; //successfull position
+				}
 			}
 			
 			//failed position
@@ -150,6 +155,8 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con
 					rmg::Object monster(*monsterType);
 					monster.setPosition(guardPos);
 					manager.placeObject(monster, false, true);
+					//Place objects away from the monster in the other zone, too
+					otherZone->getModificator<ObjectManager>()->updateDistances(monster);
 				}
 				else
 				{
@@ -225,8 +232,10 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 			rmg::Path path1 = manager.placeAndConnectObject(commonArea, rmgGate1, [this, minDist, &path2, &rmgGate1, &zShift, guarded2, &managerOther, &rmgGate2	](const int3 & tile)
 			{
 				auto ti = map.getTile(tile);
+				auto otherTi = map.getTile(tile - zShift);
 				float dist = ti.getNearestObjectDistance();
-				if(dist < minDist)
+				float otherDist = otherTi.getNearestObjectDistance();
+				if(dist < minDist || otherDist < minDist)
 					return -1.f;
 				
 				rmg::Area toPlace(rmgGate1.getArea() + rmgGate1.getAccessibleArea());
@@ -234,8 +243,8 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 				
 				path2 = managerOther.placeAndConnectObject(toPlace, rmgGate2, minDist, guarded2, true, ObjectManager::OptimizeType::NONE);
 				
-				return path2.valid() ? 1.f : -1.f;
-			}, guarded1, true, ObjectManager::OptimizeType::NONE);
+				return path2.valid() ? (dist + otherDist) : -1.f;
+			}, guarded1, true, ObjectManager::OptimizeType::DISTANCE);
 			
 			if(path1.valid() && path2.valid())
 			{

+ 20 - 15
lib/rmg/WaterProxy.cpp

@@ -209,24 +209,28 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, RouteInfo & info)
 	auto * manager = zone.getModificator<ObjectManager>();
 	if(!manager)
 		return false;
-	
+
 	auto subObjects = VLC->objtypeh->knownSubObjects(Obj::BOAT);
 	auto * boat = dynamic_cast<CGBoat *>(VLC->objtypeh->getHandlerFor(Obj::BOAT, *RandomGeneratorUtil::nextItem(subObjects, generator.rand))->create());
 
 	rmg::Object rmgObject(*boat);
 	rmgObject.setTemplate(zone.getTerrainType());
-	
+
 	auto waterAvailable = zone.areaPossible() + zone.freePaths();
 	rmg::Area coast = lake.neighbourZones.at(land.getId()); //having land tiles
 	coast.intersect(land.areaPossible() + land.freePaths()); //having only available land tiles
-	auto boardingPositions = coast.getSubarea([&waterAvailable](const int3 & tile) //tiles where boarding is possible
-	{
-		rmg::Area a({tile});
-		a = a.getBorderOutside();
-		a.intersect(waterAvailable);
-		return !a.empty();
-	});
-	
+	auto boardingPositions = coast.getSubarea([&waterAvailable, this](const int3 & tile) //tiles where boarding is possible
+		{
+			//We don't want place boat right to any land object, especiallly the zone guard
+			if (map.getTile(tile).getNearestObjectDistance() <= 3)
+				return false;
+
+			rmg::Area a({tile});
+			a = a.getBorderOutside();
+			a.intersect(waterAvailable);
+			return !a.empty();
+		});
+
 	while(!boardingPositions.empty())
 	{
 		auto boardingPosition = *boardingPositions.getTiles().begin();
@@ -239,27 +243,28 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, RouteInfo & info)
 			boardingPositions.erase(boardingPosition);
 			continue;
 		}
-		
+
 		//try to place boat at water, create paths on water and land
-		auto path = manager->placeAndConnectObject(shipPositions, rmgObject, 2, false, true, ObjectManager::OptimizeType::NONE);
+		auto path = manager->placeAndConnectObject(shipPositions, rmgObject, 4, false, true, ObjectManager::OptimizeType::NONE);
 		auto landPath = land.searchPath(boardingPosition, false);
 		if(!path.valid() || !landPath.valid())
 		{
 			boardingPositions.erase(boardingPosition);
 			continue;
 		}
-		
+
 		info.blocked = rmgObject.getArea();
 		info.visitable = rmgObject.getVisitablePosition();
 		info.boarding = boardingPosition;
 		info.water = shipPositions;
-		
+
 		zone.connectPath(path);
 		land.connectPath(landPath);
 		manager->placeObject(rmgObject, false, true);
+		land.getModificator<ObjectManager>()->updateDistances(rmgObject); //Keep land objects away from the boat
 		break;
 	}
-	
+
 	return !boardingPositions.empty();
 }
 

+ 3 - 3
server/CGameHandler.cpp

@@ -1233,7 +1233,7 @@ int64_t CGameHandler::applyBattleEffects(BattleAttack & bat, std::shared_ptr<bat
 		bai.unluckyStrike  = bat.unlucky();
 
 		auto range = gs->curB->calculateDmgRange(bai);
-		bsa.damageAmount = gs->curB->getActualDamage(range, attackerState->getCount(), getRandomGenerator());
+		bsa.damageAmount = gs->curB->getActualDamage(range.damage, attackerState->getCount(), getRandomGenerator());
 		CStack::prepareAttacked(bsa, getRandomGenerator(), bai.defender->acquireState()); //calculate casualties
 	}
 
@@ -4910,8 +4910,8 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 			const CStack * stack = gs->curB->battleGetStackByID(ba.stackNumber);
 			SpellID spellID = SpellID(ba.actionSubtype);
 
-			std::shared_ptr<const Bonus> randSpellcaster = stack->getBonusLocalFirst(Selector::type()(Bonus::RANDOM_SPELLCASTER));
-			std::shared_ptr<const Bonus> spellcaster = stack->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPELLCASTER, spellID));
+			std::shared_ptr<const Bonus> randSpellcaster = stack->getBonus(Selector::type()(Bonus::RANDOM_SPELLCASTER));
+			std::shared_ptr<const Bonus> spellcaster = stack->getBonus(Selector::typeSubtype(Bonus::SPELLCASTER, spellID));
 
 			//TODO special bonus for genies ability
 			if (randSpellcaster && battleGetRandomStackSpell(getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0)