Переглянути джерело

Merge branch 'develop' into feature/nullkiller2

Mircea TheHonestCTO 2 тижнів тому
батько
коміт
5c6f2faebb
66 змінених файлів з 1697 додано та 939 видалено
  1. 7 0
      AI/Nullkiller/AIGateway.cpp
  2. 1 0
      AI/Nullkiller/AIGateway.h
  3. 7 0
      AI/VCAI/VCAI.cpp
  4. 1 0
      AI/VCAI/VCAI.h
  5. 8 0
      Mods/vcmi/Content/Sprites/lobby/checkboxSmall.json
  6. BIN
      Mods/vcmi/Content/Sprites/lobby/checkboxSmallOff.png
  7. BIN
      Mods/vcmi/Content/Sprites/lobby/checkboxSmallOn.png
  8. BIN
      Mods/vcmi/Content/Sprites/lobby/removeChannel.png
  9. 27 2
      Mods/vcmi/Content/config/english.json
  10. 26 1
      Mods/vcmi/Content/config/german.json
  11. 2 2
      client/CMakeLists.txt
  12. 1 0
      client/CServerHandler.cpp
  13. 1 0
      client/CServerHandler.h
  14. 1 0
      client/ClientNetPackVisitors.h
  15. 7 0
      client/NetPacksClient.cpp
  16. 4 7
      client/NetPacksLobbyClient.cpp
  17. 2 2
      client/adventureMap/AdventureMapInterface.cpp
  18. 16 51
      client/battle/BattleActionsController.cpp
  19. 0 1
      client/battle/BattleActionsController.h
  20. 43 178
      client/battle/BattleFieldController.cpp
  21. 2 7
      client/battle/BattleFieldController.h
  22. 1 0
      client/gui/Shortcut.h
  23. 1 0
      client/gui/ShortcutHandler.cpp
  24. 0 517
      client/lobby/BattleOnlyMode.cpp
  25. 975 0
      client/lobby/BattleOnlyModeTab.cpp
  26. 33 17
      client/lobby/BattleOnlyModeTab.h
  27. 43 5
      client/lobby/CLobbyScreen.cpp
  28. 1 0
      client/lobby/CLobbyScreen.h
  29. 19 0
      client/lobby/CSelectionBase.cpp
  30. 3 0
      client/lobby/CSelectionBase.h
  31. 5 4
      client/lobby/SelectionTab.cpp
  32. 12 9
      client/mainmenu/CMainMenu.cpp
  33. 1 1
      client/mainmenu/CMainMenu.h
  34. 14 1
      client/render/AssetGenerator.cpp
  35. 15 0
      client/renderSDL/RenderHandler.cpp
  36. 1 1
      client/renderSDL/ScalableImage.cpp
  37. 17 2
      client/widgets/CComponent.cpp
  38. 10 1
      client/widgets/CTextInput.cpp
  39. 4 0
      client/widgets/Images.cpp
  40. 7 6
      client/widgets/TextControls.cpp
  41. 6 3
      client/windows/GUIClasses.cpp
  42. 1 0
      config/keyBindingsConfig.json
  43. 2 1
      docs/players/Cheat_Codes.md
  44. 0 15
      lib/CStack.cpp
  45. 0 2
      lib/CStack.h
  46. 71 1
      lib/StartInfo.cpp
  47. 19 4
      lib/StartInfo.h
  48. 131 53
      lib/battle/CBattleInfoCallback.cpp
  49. 17 5
      lib/battle/CBattleInfoCallback.h
  50. 16 3
      lib/battle/Unit.cpp
  51. 3 0
      lib/battle/Unit.h
  52. 2 9
      lib/bonuses/Limiters.cpp
  53. 1 0
      lib/callback/IGameEventsReceiver.h
  54. 7 0
      lib/gameState/GameStatePackVisitor.cpp
  55. 1 0
      lib/gameState/GameStatePackVisitor.h
  56. 1 0
      lib/networkPacks/NetPackVisitor.h
  57. 5 0
      lib/networkPacks/NetPacksLib.cpp
  58. 16 0
      lib/networkPacks/PacksForClientBattle.h
  59. 1 0
      lib/serializer/RegisterTypes.h
  60. 23 23
      lib/texts/Languages.h
  61. 1 1
      server/battles/BattleActionProcessor.cpp
  62. 13 3
      server/battles/BattleFlowProcessor.cpp
  63. 8 0
      server/battles/BattleResultProcessor.cpp
  64. 32 1
      server/processors/PlayerMessageProcessor.cpp
  65. 1 0
      server/processors/PlayerMessageProcessor.h
  66. 1 0
      test/vcai/mock_VCAI.h

+ 7 - 0
AI/Nullkiller/AIGateway.cpp

@@ -481,6 +481,13 @@ void AIGateway::heroSecondarySkillChanged(const CGHeroInstance * hero, int which
 }
 
 void AIGateway::battleResultsApplied()
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+	assert(status.getBattle() == ENDING_BATTLE);
+}
+
+void AIGateway::battleEnded()
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;

+ 1 - 0
AI/Nullkiller/AIGateway.h

@@ -141,6 +141,7 @@ public:
 	void heroManaPointsChanged(const CGHeroInstance * hero) override;
 	void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override;
 	void battleResultsApplied() override;
+	void battleEnded() override;
 	void beforeObjectPropertyChanged(const SetObjectProperty * sop) override;
 	void objectPropertyChanged(const SetObjectProperty * sop) override;
 	void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override;

+ 7 - 0
AI/VCAI/VCAI.cpp

@@ -548,6 +548,13 @@ void VCAI::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int
 }
 
 void VCAI::battleResultsApplied()
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+	assert(status.getBattle() == ENDING_BATTLE);
+}
+
+void VCAI::battleEnded()
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;

+ 1 - 0
AI/VCAI/VCAI.h

@@ -182,6 +182,7 @@ public:
 	void heroManaPointsChanged(const CGHeroInstance * hero) override;
 	void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override;
 	void battleResultsApplied() override;
+	void battleEnded() override;
 	void beforeObjectPropertyChanged(const SetObjectProperty * sop) override;
 	void objectPropertyChanged(const SetObjectProperty * sop) override;
 	void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override;

+ 8 - 0
Mods/vcmi/Content/Sprites/lobby/checkboxSmall.json

@@ -0,0 +1,8 @@
+{
+	"basepath" : "lobby/",
+	"images" :
+	[
+		{ "frame" : 0, "file" : "checkboxSmallOff.png"},
+		{ "frame" : 1, "file" : "checkboxSmallOn.png"}
+	]
+}

BIN
Mods/vcmi/Content/Sprites/lobby/checkboxSmallOff.png


BIN
Mods/vcmi/Content/Sprites/lobby/checkboxSmallOn.png


BIN
Mods/vcmi/Content/Sprites/lobby/removeChannel.png


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

@@ -138,13 +138,37 @@
 	"vcmi.lobby.deleteFile" : "Do you want to delete following file?",
 	"vcmi.lobby.deleteFolder" : "Do you want to delete following folder?",
 	"vcmi.lobby.deleteMode" : "Switch to delete mode and back",
-	"vcmi.lobby.battleOnlyMode" : "Battle only mode",
+	"vcmi.lobby.battleOnlyMode" : "Battle Only Mode",
+	"vcmi.lobby.battleOnlyModeSubTitle" : "Select heroes, army, skills, artifact and battleground for simple battle without adventure map",
 	"vcmi.lobby.battleOnlyModeBattlefield" : "Battlefield",
 	"vcmi.lobby.battleOnlyModeBattlefieldSelect" : "Select Battlefield",
 	"vcmi.lobby.battleOnlyModeHeroSelect" : "Select Hero",
 	"vcmi.lobby.battleOnlyModeCreatureSelect" : "Select Creature",
-	"vcmi.lobby.battleOnlyModeSelect" : "Select",
+	"vcmi.lobby.battleOnlyModeSecSkillSelect" : "Select Secondary Skill",
+	"vcmi.lobby.battleOnlyModeArtifactSelect" : "Select Artifact",
+	"vcmi.lobby.battleOnlyModeSelectHero" : "Select\nHero",
+	"vcmi.lobby.battleOnlyModeSelectUnit" : "Unit\n%d",
+	"vcmi.lobby.battleOnlyModeSelectSkill" : "Skill\n%d",
+	"vcmi.lobby.battleOnlyModeSelectArtifact" : "Art.\n%s",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.0" : "Head",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.1" : "Should.",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.2" : "Neck",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.3" : "R.Hand",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.4" : "L.Hand",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.5" : "Torso",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.6" : "R.Ring",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.7" : "L.Ring",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.8" : "Feet",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.9" : "Misc 1",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.10" : "Misc 2",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.11" : "Misc 3",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.12" : "Misc 4",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.18" : "Misc 5",
 	"vcmi.lobby.battleOnlyModeReset" : "Reset",
+	"vcmi.lobby.battleOnlySpellSelect" : "Add spells to spellbook",
+	"vcmi.lobby.battleOnlySpellSelectCurrent" : "Add or remove spells from spellbook\n\nCurrent spells:",
+	"vcmi.lobby.battleOnlySpellAdd" : "Add spell",
+	"vcmi.lobby.battleOnlySpellRemove" : "Remove spell",
 	"vcmi.lobby.templatesSelect.hover" : "Templates",
 	"vcmi.lobby.templatesSelect.help" : "Search and select template",
 
@@ -436,6 +460,7 @@
 	"vcmi.keyBindings.keyBinding.lobbyToggleChat": "Lobby toggle chat",
 	"vcmi.keyBindings.keyBinding.lobbyTurnOptions": "Lobby turn options",
 	"vcmi.keyBindings.keyBinding.lobbyCampaignSets": "Lobby campaign sets",
+	"vcmi.keyBindings.keyBinding.lobbyBattleMode": "Lobby battle mode",
 	"vcmi.keyBindings.keyBinding.mainMenuBack": "Main menu back",
 	"vcmi.keyBindings.keyBinding.mainMenuCampaign": "Main menu campaign",
 	"vcmi.keyBindings.keyBinding.mainMenuCampaignAb": "Main menu campaign ab",

+ 26 - 1
Mods/vcmi/Content/config/german.json

@@ -139,12 +139,36 @@
 	"vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?",
 	"vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück",
 	"vcmi.lobby.battleOnlyMode" : "Nur Kämpfen Modus",
+	"vcmi.lobby.battleOnlyModeSubTitle" : "Wähle Helden, Armeen, Skills, Artefakte und ein Schlachtfeld für einen einfachen Kampf ohne Abenteuerkarte",
 	"vcmi.lobby.battleOnlyModeBattlefield" : "Schlachtfeld",
 	"vcmi.lobby.battleOnlyModeBattlefieldSelect" : "Schlachtfeld auswählen",
 	"vcmi.lobby.battleOnlyModeHeroSelect" : "Helden auswählen",
 	"vcmi.lobby.battleOnlyModeCreatureSelect" : "Kreatur auswählen",
-	"vcmi.lobby.battleOnlyModeSelect" : "Wählen",
+	"vcmi.lobby.battleOnlyModeSecSkillSelect" : "Sekundären Skill auswählen",
+	"vcmi.lobby.battleOnlyModeArtifactSelect" : "Artefakt auswählen",
+	"vcmi.lobby.battleOnlyModeSelectHero" : "Wähle\nHelden",
+	"vcmi.lobby.battleOnlyModeSelectUnit" : "Einh.\n%d",
+	"vcmi.lobby.battleOnlyModeSelectSkill" : "Skill\n%d",
+	"vcmi.lobby.battleOnlyModeSelectArtifact" : "Art.\n%s",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.0" : "Kopf",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.1" : "Schult.",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.2" : "Nacken",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.3" : "R.Hand",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.4" : "L.Hand",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.5" : "Torso",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.6" : "R.Ring",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.7" : "L.Ring",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.8" : "Fuß",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.9" : "Misc 1",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.10" : "Misc 2",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.11" : "Misc 3",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.12" : "Misc 4",
+	"vcmi.lobby.battleOnlyModeSelectArtifact.18" : "Misc 5",
 	"vcmi.lobby.battleOnlyModeReset" : "Zurücksetzen",
+	"vcmi.lobby.battleOnlySpellSelect" : "Zauber zum Zauberbuch hinzufügen",
+	"vcmi.lobby.battleOnlySpellSelectCurrent" : "Zauber zum Zauberbuch hinzufügen oder entfernen\n\nAktuelle Zauber:",
+	"vcmi.lobby.battleOnlySpellAdd" : "Zauber hinzufügen",
+	"vcmi.lobby.battleOnlySpellRemove" : "Zauber entfernen",
 	"vcmi.lobby.templatesSelect.hover" : "Templates",
 	"vcmi.lobby.templatesSelect.help" : "Suche und wähle Template aus",
 
@@ -444,6 +468,7 @@
 	"vcmi.keyBindings.keyBinding.lobbyToggleChat": "Lobby Chat umschalten",
 	"vcmi.keyBindings.keyBinding.lobbyTurnOptions": "Lobby Zugoptionen",
 	"vcmi.keyBindings.keyBinding.lobbyCampaignSets": "Lobby Kampagnensätze",
+	"vcmi.keyBindings.keyBinding.lobbyBattleMode": "Lobby Kampfmodus",
 	"vcmi.keyBindings.keyBinding.mainMenuBack": "Hauptmenü zurück",
 	"vcmi.keyBindings.keyBinding.mainMenuCampaign": "Hauptmenü Kampagne",
 	"vcmi.keyBindings.keyBinding.mainMenuCampaignAb": "Hauptmenü Kampagne Ab",

+ 2 - 2
client/CMakeLists.txt

@@ -52,7 +52,7 @@ set(vcmiclientcommon_SRCS
 	gui/ShortcutHandler.cpp
 	gui/WindowHandler.cpp
 
-	lobby/BattleOnlyMode.cpp
+	lobby/BattleOnlyModeTab.cpp
 	lobby/CBonusSelection.cpp
 	lobby/CCampaignInfoScreen.cpp
 	lobby/CLobbyScreen.cpp
@@ -263,7 +263,7 @@ set(vcmiclientcommon_HEADERS
 	gui/TextAlignment.h
 	gui/WindowHandler.h
 
-	lobby/BattleOnlyMode.h
+	lobby/BattleOnlyModeTab.h
 	lobby/CBonusSelection.h
 	lobby/CCampaignInfoScreen.h
 	lobby/CLobbyScreen.h

+ 1 - 0
client/CServerHandler.cpp

@@ -107,6 +107,7 @@ CServerHandler::CServerHandler()
 	, screenType(ESelectionScreen::unknown)
 	, serverMode(EServerMode::NONE)
 	, loadMode(ELoadMode::NONE)
+	, battleMode(false)
 	, client(nullptr)
 {
 	uuid = boost::uuids::to_string(boost::uuids::random_generator()());

+ 1 - 0
client/CServerHandler.h

@@ -140,6 +140,7 @@ public:
 	ESelectionScreen screenType; // To create lobby UI only after server is setup
 	EServerMode serverMode;
 	ELoadMode loadMode; // For saves filtering in SelectionTab
+	bool battleMode;
 	////////////////////
 
 	std::unique_ptr<CStopWatch> th;

+ 1 - 0
client/ClientNetPackVisitors.h

@@ -86,6 +86,7 @@ public:
 	void visitSetStackEffect(SetStackEffect & pack) override;
 	void visitStacksInjured(StacksInjured & pack) override;
 	void visitBattleResultsApplied(BattleResultsApplied & pack) override;
+	void visitBattleEnded(BattleEnded & pack) override;
 	void visitBattleUnitsChanged(BattleUnitsChanged & pack) override;
 	void visitBattleObstaclesChanged(BattleObstaclesChanged & pack) override;
 	void visitCatapultAttack(CatapultAttack & pack) override;

+ 7 - 0
client/NetPacksClient.cpp

@@ -871,6 +871,13 @@ void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied &
 	callInterfaceIfPresent(cl, PlayerColor::SPECTATOR, &IGameEventsReceiver::battleResultsApplied);
 }
 
+void ApplyClientNetPackVisitor::visitBattleEnded(BattleEnded & pack)
+{
+	callInterfaceIfPresent(cl, pack.victor, &IGameEventsReceiver::battleEnded);
+	callInterfaceIfPresent(cl, pack.loser, &IGameEventsReceiver::battleEnded);
+	callInterfaceIfPresent(cl, PlayerColor::SPECTATOR, &IGameEventsReceiver::battleEnded);
+}
+
 void ApplyClientNetPackVisitor::visitBattleUnitsChanged(BattleUnitsChanged & pack)
 {
 	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleUnitsChanged, pack.battleID, pack.changedStacks);

+ 4 - 7
client/NetPacksLobbyClient.cpp

@@ -19,7 +19,7 @@
 #include "lobby/ExtraOptionsTab.h"
 #include "lobby/SelectionTab.h"
 #include "lobby/CBonusSelection.h"
-#include "lobby/BattleOnlyMode.h"
+#include "lobby/BattleOnlyModeTab.h"
 #include "globalLobby/GlobalLobbyWindow.h"
 #include "globalLobby/GlobalLobbyServerSetup.h"
 #include "globalLobby/GlobalLobbyClient.h"
@@ -114,9 +114,6 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack
 	if(!lobby || !handler.isGuest())
 		return;
 
-	if(auto topWindow = ENGINE->windows().topWindow<BattleOnlyModeWindow>())
-		topWindow->close();
-
 	switch(pack.action)
 	{
 	case LobbyGuiAction::NO_TAB:
@@ -138,7 +135,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack
 		lobby->toggleTab(lobby->tabExtraOptions);
 		break;
 	case LobbyGuiAction::BATTLE_MODE:
-		BattleOnlyMode::openBattleWindow();
+		lobby->toggleTab(lobby->tabBattleOnlyMode);
 		break;
 	}
 }
@@ -242,6 +239,6 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyShowMessage(LobbyShowMessage &
 
 void ApplyOnLobbyScreenNetPackVisitor::visitLobbySetBattleOnlyModeStartInfo(LobbySetBattleOnlyModeStartInfo & pack)
 {
-	if(auto topWindow = ENGINE->windows().topWindow<BattleOnlyModeWindow>())
-		topWindow->applyStartInfo(pack.startInfo);
+	if(lobby->tabBattleOnlyMode)
+		lobby->tabBattleOnlyMode->applyStartInfo(pack.startInfo);
 }

+ 2 - 2
client/adventureMap/AdventureMapInterface.cpp

@@ -639,14 +639,14 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
 		std::string text = GAME->interface()->localState->getCurrentHero() ? objAtTile->getHoverText(GAME->interface()->localState->getCurrentHero()) : objAtTile->getHoverText(GAME->interface()->playerID);
 		boost::replace_all(text,"\n"," ");
 		if (ENGINE->isKeyboardCmdDown())
-			text.append(" (" + std::to_string(targetPosition.x) + ", " + std::to_string(targetPosition.y) + ")");
+			text.append(" (" + std::to_string(targetPosition.x) + ", " + std::to_string(targetPosition.y) + ", " + std::to_string(targetPosition.z) + ")");
 		ENGINE->statusbar()->write(text);
 	}
 	else if(isTargetPositionVisible)
 	{
 		std::string tileTooltipText = GAME->map().getTerrainDescr(targetPosition, false);
 		if (ENGINE->isKeyboardCmdDown())
-			tileTooltipText.append(" (" + std::to_string(targetPosition.x) + ", " + std::to_string(targetPosition.y) + ")");
+			tileTooltipText.append(" (" + std::to_string(targetPosition.x) + ", " + std::to_string(targetPosition.y) + ", " + std::to_string(targetPosition.z) + ")");
 		ENGINE->statusbar()->write(tileTooltipText);
 	}
 

+ 16 - 51
client/battle/BattleActionsController.cpp

@@ -527,7 +527,8 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
 			{
 				const auto * attacker = owner.stacksController->getActiveStack();
-				BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
+				BattleHex attackFromHex = owner.getBattle()->fromWhichHexAttack(attacker, targetHex, owner.fieldController->selectAttackDirection(targetHex));
+				assert(attackFromHex.isValid());
 				int distance = attacker->position.isValid() ? owner.getBattle()->battleGetDistances(attacker, attacker->getPosition())[attackFromHex.toInt()] : 0;
 				DamageEstimation retaliation;
 				BattleAttackInfo attackInfo(attacker, targetStack, distance, false );
@@ -644,8 +645,8 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, c
 		case PossiblePlayerBattleAction::MOVE_STACK:
 			if (!(targetStack && targetStack->alive())) //we can walk on dead stacks
 			{
-				if(canStackMoveHere(owner.stacksController->getActiveStack(), targetHex))
-					return true;
+				const CStack * currentStack = owner.stacksController->getActiveStack();
+				return currentStack && owner.getBattle()->toWhichHexMove(currentStack, targetHex).isValid();
 			}
 			return false;
 
@@ -653,16 +654,11 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, c
 		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
 			{
-				auto activeStack = owner.stacksController->getActiveStack();
-				if (targetStack && targetStack != activeStack && owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack?
-				{
-					BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
-					if(owner.getBattle()->battleCanAttack(activeStack, targetStack, attackFromHex))
-						return true;
-				}
-				return false;
+				const CStack * currentStack = owner.stacksController->getActiveStack();
+				return currentStack &&
+					owner.getBattle()->battleCanAttackUnit(currentStack, targetStack) &&
+					owner.getBattle()->battleCanAttackHex(currentStack, targetHex);
 			}
-
 		case PossiblePlayerBattleAction::SHOOT:
 			{
 				auto currentStack = owner.stacksController->getActiveStack();
@@ -740,26 +736,9 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, c
 		case PossiblePlayerBattleAction::MOVE_STACK:
 		{
 			const auto * activeStack = owner.stacksController->getActiveStack();
-
-			if(activeStack->doubleWide())
-			{
-				BattleHexArray availableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false);
-				BattleHex shiftedDest = targetHex.cloneInDirection(activeStack->destShiftDir(), false);
-				const bool canMoveHeadHere = availableHexes.contains(targetHex);
-				const bool canMoveTailHere = availableHexes.contains(shiftedDest);
-				const bool backwardsMove = activeStack->unitSide() == BattleSide::ATTACKER ?
-											   targetHex.getX() < activeStack->getPosition().getX():
-											   targetHex.getX() > activeStack->getPosition().getX();
-
-				if(canMoveTailHere && (backwardsMove || !canMoveHeadHere))
-					owner.giveCommand(EActionType::WALK, shiftedDest);
-				else
-					owner.giveCommand(EActionType::WALK, targetHex);
-			}
-			else
-			{
-				owner.giveCommand(EActionType::WALK, targetHex);
-			}
+			auto toHex = owner.getBattle()->toWhichHexMove(activeStack, targetHex);
+			assert(toHex.isValid());
+			owner.giveCommand(EActionType::WALK, toHex);
 			return;
 		}
 
@@ -768,12 +747,11 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, c
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable 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)
-			{
-				BattleAction command = BattleAction::makeMeleeAttack(owner.stacksController->getActiveStack(), targetHex, attackFromHex, returnAfterAttack);
-				owner.sendCommand(command, owner.stacksController->getActiveStack());
-			}
+			auto attacker = owner.stacksController->getActiveStack();
+			BattleHex attackFromHex = owner.getBattle()->fromWhichHexAttack(attacker, targetHex, owner.fieldController->selectAttackDirection(targetHex));
+			assert(attackFromHex.isValid());
+			BattleAction command = BattleAction::makeMeleeAttack(attacker, targetHex, attackFromHex, returnAfterAttack);
+			owner.sendCommand(command, attacker);
 			return;
 		}
 
@@ -1032,19 +1010,6 @@ bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell,
 	return m->canBeCastAt(target, problem);
 }
 
-bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, const BattleHex & myNumber) const
-{
-	BattleHexArray acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false);
-	BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false);
-
-	if (acc.contains(myNumber))
-		return true;
-	else if (stackToMove->doubleWide() && acc.contains(shiftedDest))
-		return true;
-	else
-		return false;
-}
-
 void BattleActionsController::activateStack()
 {
 	const CStack * s = owner.stacksController->getActiveStack();

+ 0 - 1
client/battle/BattleActionsController.h

@@ -45,7 +45,6 @@ class BattleActionsController
 	const CStack * selectedStack;
 
 	bool isCastingPossibleHere (const CSpell * spell, const CStack *shere, const BattleHex & myNumber);
-	bool canStackMoveHere (const CStack *sactive, const 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, const CStack * targetStack);
 

+ 43 - 178
client/battle/BattleFieldController.cpp

@@ -263,9 +263,10 @@ void BattleFieldController::showBackgroundImageWithHexes(Canvas & canvas)
 void BattleFieldController::redrawBackgroundWithHexes()
 {
 	const CStack *activeStack = owner.stacksController->getActiveStack();
-	BattleHexArray attackableHexes;
 	if(activeStack)
-		occupiableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false, true, &attackableHexes);
+		availableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false);
+	else
+		availableHexes.clear();
 
 	// prepare background graphic with hexes and shaded hexes
 	backgroundWithHexes->draw(background, Point(0,0));
@@ -274,13 +275,16 @@ void BattleFieldController::redrawBackgroundWithHexes()
 		owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes);
 
 	// show shaded hexes for active's stack valid movement and the hexes that it can attack
-	if(settings["battle"]["stackRange"].Bool())
+	if(activeStack && settings["battle"]["stackRange"].Bool())
 	{
-		BattleHexArray hexesToShade = occupiableHexes;
-		hexesToShade.insert(attackableHexes);
-		for(const BattleHex & hex : hexesToShade)
+		auto occupiableHexes = owner.getBattle()->battleGetOccupiableHexes(availableHexes, activeStack);
+		for(si16 hex = 0; hex < GameConstants::BFIELD_SIZE; hex++)
 		{
-			showHighlightedHex(*backgroundWithHexes, cellShade, hex, false);
+			//shade occupiable and attackable hexes
+			if (occupiableHexes.contains(hex) ||
+				(owner.getBattle()->battleCanAttackUnit(activeStack, owner.getBattle()->battleGetStackByPos(hex, true)) &&
+					owner.getBattle()->battleCanAttackHex(availableHexes, activeStack, hex)))
+				showHighlightedHex(*backgroundWithHexes, cellShade, hex, false);
 		}
 	}
 
@@ -330,10 +334,7 @@ BattleHexArray BattleFieldController::getMovementRangeForHoveredStack()
 		return BattleHexArray();
 
 	auto hoveredStack = getHoveredStack();
-	if(hoveredStack)
-		return owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr);
-	else
-		return BattleHexArray();
+	return hoveredStack ? owner.getBattle()->battleGetOccupiableHexes(hoveredStack, true) : BattleHexArray();
 }
 
 BattleHexArray BattleFieldController::getHighlightedHexesForSpellRange()
@@ -371,45 +372,26 @@ BattleHexArray BattleFieldController::getHighlightedHexesForMovementTarget()
 	if(!stack)
 		return {};
 
-	BattleHexArray availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr);
-
 	auto hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
 
-	if(owner.getBattle()->battleCanAttack(stack, hoveredStack, hoveredHex) && isTileAttackable(hoveredHex))
+	if(owner.getBattle()->battleCanAttackUnit(stack, hoveredStack) && owner.getBattle()->battleCanAttackHex(availableHexes, stack, hoveredHex))
 	{
-		BattleHex attackFromHex = fromWhichHexAttack(hoveredHex);
-		if(owner.getBattle()->battleCanAttack(stack, hoveredStack, attackFromHex))
-		{
-			if(stack->doubleWide())
-				return {attackFromHex, stack->occupiedHex(attackFromHex)};
+		BattleHex fromHex = owner.getBattle()->fromWhichHexAttack(stack, hoveredHex, selectAttackDirection(hoveredHex));
+		assert(fromHex.isValid());
+		if(stack->doubleWide())
+			return {fromHex, stack->occupiedHex(fromHex)};
 
-			return {attackFromHex};
-		}
+		return {fromHex};
 	}
 
-	if (stack->doubleWide())
-	{
-		const bool canMoveHeadHere = hoveredHex.isAvailable() && availableHexes.contains(hoveredHex);
-		const bool canMoveTailHere = hoveredHex.isAvailable() && availableHexes.contains(hoveredHex.cloneInDirection(stack->destShiftDir()));
-		const bool backwardsMove = stack->unitSide() == BattleSide::ATTACKER ?
-									   hoveredHex.getX() < stack->getPosition().getX():
-									   hoveredHex.getX() > stack->getPosition().getX();
-
-		if(canMoveTailHere && (backwardsMove || !canMoveHeadHere))
-			return {hoveredHex, hoveredHex.cloneInDirection(stack->destShiftDir())};
-
-		if (canMoveHeadHere)
-			return {hoveredHex, stack->occupiedHex(hoveredHex)};
-
+	auto toHex = owner.getBattle()->toWhichHexMove(availableHexes, stack, hoveredHex);
+	if (!toHex.isValid())
 		return {};
-	}
+	
+	if (stack->doubleWide())
+		return {toHex, stack->occupiedHex(toHex)};
 	else
-	{
-		if (availableHexes.contains(hoveredHex))
-			return {hoveredHex};
-		else
-			return {};
-	}
+		return {toHex};
 }
 
 // Range limit highlight helpers
@@ -667,162 +649,45 @@ BattleHex BattleFieldController::getHexAtPosition(Point hoverPos)
 
 BattleHex::EDir BattleFieldController::selectAttackDirection(const BattleHex & myNumber) const
 {
-	const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide();
+	auto attacker = owner.stacksController->getActiveStack();
+	assert(attacker);
 	const BattleHexArray & neighbours = myNumber.getAllNeighbouringTiles();
-	//   0 1
-	//  5 x 2
-	//   4 3
-
-	// if true - our current stack can move into this hex (and attack)
-	std::array<bool, 8> attackAvailability;
-
-	if (doubleWide)
-	{
-		// For double-hexes we need to ensure that both hexes needed for this direction are occupyable:
-		// |    -0-   |   -1-    |    -2-   |   -3-    |    -4-   |   -5-    |    -6-   |   -7-
-		// |  o o -   |   - o o  |    - -   |   - -    |    - -   |   - -    |    o o   |   - -
-		// |   - x -  |  - x -   |   - x o o|  - x -   |   - x -  |o o x -   |   - x -  |  - x -
-		// |    - -   |   - -    |    - -   |   - o o  |  o o -   |   - -    |    - -   |   o o
-
-		for (size_t i : { 1, 2, 3})
-		{
-			BattleHex target = neighbours[i].cloneInDirection(BattleHex::RIGHT, false);
-			attackAvailability[i] = neighbours[i].isValid() && occupiableHexes.contains(neighbours[i]) && target.isValid() && occupiableHexes.contains(target);
-		}
-
-		for (size_t i : { 4, 5, 0})
-		{
-			BattleHex target = neighbours[i].cloneInDirection(BattleHex::LEFT, false);
-			attackAvailability[i] = neighbours[i].isValid() && occupiableHexes.contains(neighbours[i]) && target.isValid() && occupiableHexes.contains(target);
-		}
-
-		attackAvailability[6] = neighbours[0].isValid() && neighbours[1].isValid() && occupiableHexes.contains(neighbours[0]) && occupiableHexes.contains(neighbours[1]);
-		attackAvailability[7] = neighbours[3].isValid() && neighbours[4].isValid() && occupiableHexes.contains(neighbours[3]) && occupiableHexes.contains(neighbours[4]);
-	}
-	else
-	{
-		for (size_t i = 0; i < 6; ++i)
-			attackAvailability[i] = neighbours[i].isValid() && occupiableHexes.contains(neighbours[i]);
-
-		attackAvailability[6] = false;
-		attackAvailability[7] = false;
-	}
-
-	// Zero available tiles to attack from
-	if ( vstd::find(attackAvailability, true) == attackAvailability.end())
-	{
-		logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber);
-		return BattleHex::NONE;
-	}
-
 	// For each valid direction, select position to test against
 	std::array<Point, 8> testPoint;
+	testPoint.fill(Point::makeInvalid());
 
 	for (size_t i = 0; i < 6; ++i)
-		if (attackAvailability[i])
+		if (owner.getBattle()->battleCanAttackHex(availableHexes, attacker, myNumber, BattleHex::EDir(i)))
 			testPoint[i] = hexPositionAbsolute(neighbours[i]).center();
 
 	// For bottom/top directions select central point, but move it a bit away from true center to reduce zones allocated to them
-	if (attackAvailability[6])
+	if (owner.getBattle()->battleCanAttackHex(availableHexes, attacker, myNumber, BattleHex::EDir(6)))
 		testPoint[6] = (hexPositionAbsolute(neighbours[0]).center() + hexPositionAbsolute(neighbours[1]).center()) / 2 + Point(0, -5);
 
-	if (attackAvailability[7])
+	if (owner.getBattle()->battleCanAttackHex(availableHexes, attacker, myNumber, BattleHex::EDir(7)))
 		testPoint[7] = (hexPositionAbsolute(neighbours[3]).center() + hexPositionAbsolute(neighbours[4]).center()) / 2 + Point(0,  5);
 
 	// Compute distance between tested position & cursor position and pick nearest
-	std::array<int, 8> distance2;
-
-	for (size_t i = 0; i < 8; ++i)
-		if (attackAvailability[i])
-			distance2[i] = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x);
-
+	int nearestDistance = std::numeric_limits<int>::max();
 	size_t nearest = -1;
-	for (size_t i = 0; i < 8; ++i)
-		if (attackAvailability[i] && (nearest == -1 || distance2[i] < distance2[nearest]) )
-			nearest = i;
-
-	assert(nearest != -1);
-	return BattleHex::EDir(nearest);
-}
-
-BattleHex BattleFieldController::fromWhichHexAttack(const BattleHex & attackTarget)
-{
-	BattleHex::EDir direction = selectAttackDirection(attackTarget);
-
-	const CStack * attacker = owner.stacksController->getActiveStack();
 
-	assert(direction != BattleHex::NONE);
-	assert(attacker);
-
-	if (!attacker->doubleWide())
-	{
-		assert(direction != BattleHex::BOTTOM);
-		assert(direction != BattleHex::TOP);
-		return attackTarget.cloneInDirection(direction);
-	}
-	else
+	for (size_t i = 0; i < 8; ++i)
 	{
-		// We need to find position of right hex of double-hex creature (or left for defending side)
-		// | TOP_LEFT |TOP_RIGHT |   RIGHT  |BOTTOM_RIGHT|BOTTOM_LEFT|  LEFT    |    TOP   |BOTTOM
-		// |  o o -   |   - o o  |    - -   |   - -      |    - -    |   - -    |    o o   |   - -
-		// |   - x -  |  - x -   |   - x o o|  - x -     |   - x -   |o o x -   |   - x -  |  - x -
-		// |    - -   |   - -    |    - -   |   - o o    |  o o -    |   - -    |    - -   |   o o
-
-		switch (direction)
+		if (testPoint[i].isValid())
 		{
-		case BattleHex::TOP_LEFT:
-		case BattleHex::LEFT:
-		case BattleHex::BOTTOM_LEFT:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(direction);
-			else
-				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT);
-		}
-
-		case BattleHex::TOP_RIGHT:
-		case BattleHex::RIGHT:
-		case BattleHex::BOTTOM_RIGHT:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT);
-			else
-				return attackTarget.cloneInDirection(direction);
-		}
-
-		case BattleHex::TOP:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT);
-			else
-				return attackTarget.cloneInDirection(BattleHex::TOP_LEFT);
-		}
-
-		case BattleHex::BOTTOM:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT);
-			else
-				return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT);
-		}
-		default:
-			assert(0);
-			return BattleHex::INVALID;
+			int distance = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x);
+			if (nearest == -1 || distance < nearestDistance)
+			{
+				nearestDistance = distance;
+				nearest = i;
+			}
 		}
 	}
-}
-
-bool BattleFieldController::isTileAttackable(const BattleHex & number) const
-{
-	if(!number.isValid())
-		return false;
 
-	for (auto & elem : occupiableHexes)
-	{
-		if (BattleHex::mutualPosition(elem, number) != BattleHex::EDir::NONE)
-			return true;
-	}
-	return false;
+	if (nearest == -1)
+		// Zero available tiles to attack from
+		logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber);
+	return BattleHex::EDir(nearest);
 }
 
 void BattleFieldController::updateAccessibleHexes()

+ 2 - 7
client/battle/BattleFieldController.h

@@ -46,8 +46,8 @@ class BattleFieldController : public CIntObject
 	/// hex currently under mouse hover
 	BattleHex hoveredHex;
 
-	/// hexes to which currently active stack can move
-	BattleHexArray occupiableHexes;
+	/// hexes to which the currently active stack can move (for double-wide units only the head is considered)
+	BattleHexArray availableHexes;
 
 	/// hexes that when in front of a unit cause it's amount box to move back
 	std::array<bool, GameConstants::BFIELD_SIZE> stackCountOutsideHexes;
@@ -126,13 +126,8 @@ public:
 	/// Returns the currently hovered stack
 	const CStack* getHoveredStack();
 
-	/// returns true if selected tile can be attacked in melee by current stack
-	bool isTileAttackable(const BattleHex & number) const;
-
 	/// returns true if stack should render its stack count image in default position - outside own hex
 	bool stackCountOutsideHex(const BattleHex & number) const;
 
 	BattleHex::EDir selectAttackDirection(const BattleHex & myNumber) const;
-
-	BattleHex fromWhichHexAttack(const BattleHex & myNumber);
 };

+ 1 - 0
client/gui/Shortcut.h

@@ -98,6 +98,7 @@ enum class EShortcut
 	LOBBY_RANDOM_TOWN_VS,
 	LOBBY_HANDICAP,
 	LOBBY_CAMPAIGN_SETS,
+	LOBBY_BATTLE_MODE,
 
 	MAPS_SIZE_S,
 	MAPS_SIZE_M,

+ 1 - 0
client/gui/ShortcutHandler.cpp

@@ -162,6 +162,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"lobbyToggleChat",          EShortcut::LOBBY_TOGGLE_CHAT         },
 		{"lobbyAdditionalOptions",   EShortcut::LOBBY_ADDITIONAL_OPTIONS  },
 		{"lobbySelectScenario",      EShortcut::LOBBY_SELECT_SCENARIO     },
+		{"lobbyBattleMode",          EShortcut::LOBBY_BATTLE_MODE         },
 		{"gameEndTurn",              EShortcut::ADVENTURE_END_TURN        }, // compatibility ID - extra's use this string
 		{"adventureEndTurn",         EShortcut::ADVENTURE_END_TURN        },
 		{"adventureLoadGame",        EShortcut::ADVENTURE_LOAD_GAME       },

+ 0 - 517
client/lobby/BattleOnlyMode.cpp

@@ -1,517 +0,0 @@
-/*
- * BattleOnlyMode.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-
-#include "StdInc.h"
-#include "BattleOnlyMode.h"
-
-#include "../CServerHandler.h"
-#include "../GameEngine.h"
-#include "../GameInstance.h"
-
-#include "../render/IRenderHandler.h"
-#include "../render/CAnimation.h"
-#include "../render/Canvas.h"
-#include "../render/CanvasImage.h"
-#include "../gui/Shortcut.h"
-#include "../gui/WindowHandler.h"
-#include "../widgets/Buttons.h"
-#include "../widgets/GraphicalPrimitiveCanvas.h"
-#include "../widgets/TextControls.h"
-#include "../widgets/CTextInput.h"
-#include "../widgets/Images.h"
-#include "../windows/GUIClasses.h"
-#include "../windows/CHeroOverview.h"
-#include "../windows/CCreatureWindow.h"
-
-#include "../../lib/GameLibrary.h"
-#include "../../lib/gameState/CGameState.h"
-#include "../../lib/networkPacks/PacksForLobby.h"
-#include "../../lib/StartInfo.h"
-#include "../../lib/VCMIDirs.h"
-#include "../../lib/CRandomGenerator.h"
-#include "../../lib/callback/EditorCallback.h"
-#include "../../lib/entities/hero/CHero.h"
-#include "../../lib/entities/hero/CHeroClass.h"
-#include "../../lib/entities/hero/CHeroHandler.h"
-#include "../../lib/entities/faction/CTown.h"
-#include "../../lib/entities/faction/CTownHandler.h"
-#include "../../lib/mapObjects/CGHeroInstance.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-#include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
-#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h"
-#include "../../lib/mapping/CMap.h"
-#include "../../lib/mapping/CMapInfo.h"
-#include "../../lib/mapping/CMapEditManager.h"
-#include "../../lib/mapping/CMapService.h"
-#include "../../lib/mapping/MapFormat.h"
-#include "../../lib/texts/CGeneralTextHandler.h"
-#include "../../lib/texts/MetaString.h"
-#include "../../lib/texts/TextOperations.h"
-#include "../../lib/filesystem/Filesystem.h"
-
-void BattleOnlyMode::openBattleWindow()
-{
-	GAME->server().sendGuiAction(LobbyGuiAction::BATTLE_MODE);
-	ENGINE->windows().createAndPushWindow<BattleOnlyModeWindow>();
-}
-
-BattleOnlyModeWindow::BattleOnlyModeWindow()
-	: CWindowObject(BORDERED)
-	, startInfo(std::make_shared<BattleOnlyModeStartInfo>())
-	, disabledColor(GAME->server().isHost() ? Colors::WHITE : Colors::ORANGE)
-{
-	OBJECT_CONSTRUCTION;
-
-	pos.w = 519;
-	pos.h = 238;
-
-	updateShadow();
-	center();
-
-	init();
-
-	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
-	backgroundTexture->setPlayerColor(PlayerColor(1));
-	buttonOk = std::make_shared<CButton>(Point(191, 203), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ startBattle(); }, EShortcut::GLOBAL_ACCEPT);
-	buttonOk->block(true);
-	buttonAbort = std::make_shared<CButton>(Point(265, 203), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){
-		GAME->server().sendGuiAction(LobbyGuiAction::NO_TAB);
-		close();
-	}, EShortcut::GLOBAL_CANCEL);
-	buttonAbort->block(true);
-	title = std::make_shared<CLabel>(260, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode"));
-
-	battlefieldSelector = std::make_shared<CButton>(Point(29, 174), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){
-		std::vector<std::string> texts;
-		std::vector<std::shared_ptr<IImage>> images;
-
-		auto & terrains = LIBRARY->terrainTypeHandler->objects;
-		for (const auto & terrain : terrains)
-		{
-			if(!terrain->isPassable())
-				continue;
-
-			texts.push_back(terrain->getNameTranslated());
-
-			const auto & patterns = LIBRARY->terviewh->getTerrainViewPatterns(terrain->getId());
-			TerrainViewPattern pattern;
-			for(auto & p : patterns)
-				if(p[0].id == "n1")
-					pattern = p[0];
-			auto image = ENGINE->renderHandler().loadImage(terrain->tilesFilename, pattern.mapping[0].first, 0, EImageBlitMode::OPAQUE);
-			image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST);
-			images.push_back(image);
-		}
-
-		auto factions = LIBRARY->townh->getDefaultAllowed();
-		for (const auto & faction : factions)
-		{
-			texts.push_back(faction.toFaction()->getNameTranslated());
-
-			auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("ITPA"), faction.toFaction()->town->clientInfo.icons[true][false] + 2, 0, EImageBlitMode::OPAQUE);
-			image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST);
-			images.push_back(image);
-		}
-
-		ENGINE->windows().createAndPushWindow<CObjectListWindow>(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefield"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefieldSelect"), [this, terrains, factions](int index){
-			if(terrains.size() > index)
-			{
-				startInfo->selectedTerrain = terrains[index]->getId();
-				startInfo->selectedTown = std::nullopt;
-			}
-			else
-			{
-				startInfo->selectedTerrain = std::nullopt;
-				auto it = std::next(factions.begin(), index - terrains.size());
-				if (it != factions.end())
-    				startInfo->selectedTown = *it;
-			}
-			onChange();
-		}, (startInfo->selectedTerrain ? static_cast<int>(*startInfo->selectedTerrain) : static_cast<int>(*startInfo->selectedTown + terrains.size())), images, true, true);
-	});
-	battlefieldSelector->block(GAME->server().isGuest());
-	buttonReset = std::make_shared<CButton>(Point(289, 174), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){
-		if(GAME->server().isHost())
-		{
-			startInfo->selectedTerrain = TerrainId::DIRT;
-			startInfo->selectedTown = std::nullopt;
-			startInfo->selectedHero[0] = std::nullopt;
-			startInfo->selectedArmy[0].fill(CStackBasicDescriptor(CreatureID::NONE, 1));
-			for(size_t i=0; i<GameConstants::ARMY_SIZE; i++)
-				heroSelector1->selectedArmyInput.at(i)->disable();
-		}
-		startInfo->selectedHero[1] = std::nullopt;
-		startInfo->selectedArmy[1].fill(CStackBasicDescriptor(CreatureID::NONE, 1));
-		for(size_t i=0; i<GameConstants::ARMY_SIZE; i++)
-			heroSelector2->selectedArmyInput.at(i)->disable();
-		onChange();
-	});
-	buttonReset->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeReset"), EFonts::FONT_SMALL, Colors::WHITE);
-
-	heroSelector1 = std::make_shared<BattleOnlyModeHeroSelector>(0, *this, Point(0, 40));
-	heroSelector2 = std::make_shared<BattleOnlyModeHeroSelector>(1, *this, Point(260, 40));
-
-	heroSelector1->setInputEnabled(GAME->server().isHost());
-
-	onChange();
-}
-
-void BattleOnlyModeWindow::init()
-{
-	map = std::make_unique<CMap>(nullptr);
-	map->version = EMapFormat::VCMI;
-	map->creationDateTime = std::time(nullptr);
-	map->width = 10;
-	map->height = 10;
-	map->mapLevels = 1;
-	map->battleOnly = true;
-	map->name = MetaString::createFromTextID("vcmi.lobby.battleOnlyMode");
-
-	cb = std::make_unique<EditorCallback>(map.get());
-}
-
-void BattleOnlyModeWindow::onChange()
-{
-	GAME->server().setBattleOnlyModeStartInfo(startInfo);
-}
-
-void BattleOnlyModeWindow::update()
-{
-	setTerrainButtonText();
-	setOkButtonEnabled();
-	
-	heroSelector1->setHeroIcon();
-	heroSelector1->setCreatureIcons();
-	heroSelector2->setHeroIcon();
-	heroSelector2->setCreatureIcons();
-	redraw();
-}
-
-void BattleOnlyModeWindow::applyStartInfo(std::shared_ptr<BattleOnlyModeStartInfo> si)
-{
-	startInfo = si;
-	update();
-}
-
-void BattleOnlyModeWindow::setTerrainButtonText()
-{
-	battlefieldSelector->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefield") + ":   " + (startInfo->selectedTerrain ? (*startInfo->selectedTerrain).toEntity(LIBRARY)->getNameTranslated() : (*startInfo->selectedTown).toEntity(LIBRARY)->getNameTranslated()), EFonts::FONT_SMALL, disabledColor);
-}
-
-void BattleOnlyModeWindow::setOkButtonEnabled()
-{
-	bool army2Empty = std::all_of(startInfo->selectedArmy[1].begin(), startInfo->selectedArmy[1].end(), [](const auto x) { return x.getId() == CreatureID::NONE; });
-
-	bool canStart = (startInfo->selectedTerrain || startInfo->selectedTown);
-	canStart &= (startInfo->selectedHero[0] && ((startInfo->selectedHero[1]) || (startInfo->selectedTown && !army2Empty)));
-	buttonOk->block(!canStart || GAME->server().isGuest());
-	buttonAbort->block(GAME->server().isGuest());
-}
-
-std::shared_ptr<IImage> drawBlackBox(Point size, std::string text, ColorRGBA color)
-{
-	auto image = ENGINE->renderHandler().createImage(size, CanvasScalingPolicy::AUTO);
-	Canvas canvas = image->getCanvas();
-	canvas.drawColor(Rect(0, 0, size.x, size.y), Colors::BLACK);
-	canvas.drawText(Point(size.x / 2, size.y / 2), FONT_TINY, color, ETextAlignment::CENTER, text);
-	return image;
-}
-
-BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(int id, BattleOnlyModeWindow& p, Point position)
-: parent(p)
-, id(id)
-{
-	OBJECT_CONSTRUCTION;
-
-	pos.x += position.x;
-	pos.y += position.y;
-
-	backgroundImage = std::make_shared<CPicture>(ImagePath::builtin("heroSlotsBlue"), Point(3, 4));
-
-	for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
-	{
-		auto image = std::make_shared<CAnimImage>(AnimationPath::builtin("PSKIL32"), i, 0, 78 + i * 36, 26);
-		primSkills.push_back(image);
-		primSkillsBorder.push_back(std::make_shared<GraphicalPrimitiveCanvas>(Rect(78 + i * 36, 26, 32, 32)));
-		primSkillsBorder.back()->addRectangle(Point(0, 0), Point(32, 32), ColorRGBA(44, 108, 255));
-		primSkillsInput.push_back(std::make_shared<CTextInput>(Rect(78 + i * 36, 58, 32, 16), EFonts::FONT_SMALL, ETextAlignment::CENTER, false));
-		primSkillsInput.back()->setColor(id == 1 ? Colors::WHITE : parent.disabledColor);
-		primSkillsInput.back()->setFilterNumber(0, 100);
-		primSkillsInput.back()->setText("0");
-		primSkillsInput.back()->setCallback([this, i, id](const std::string & text){
-			parent.startInfo->primSkillLevel[id][i] = std::stoi(primSkillsInput[i]->getText());
-			parent.onChange();
-		});
-	}
-
-	creatureImage.resize(GameConstants::ARMY_SIZE);
-	for(size_t i=0; i<GameConstants::ARMY_SIZE; i++)
-	{
-		selectedArmyInput.push_back(std::make_shared<CTextInput>(Rect(5 + i * 36, 113, 32, 16), EFonts::FONT_SMALL, ETextAlignment::CENTER, false));
-		selectedArmyInput.back()->setColor(id == 1 ? Colors::WHITE : parent.disabledColor);
-		selectedArmyInput.back()->setFilterNumber(1, 10000000, 3);
-		selectedArmyInput.back()->setText("1");
-		selectedArmyInput.back()->setCallback([this, i, id](const std::string & text){
-			if(parent.startInfo->selectedArmy[id][i].getId() != CreatureID::NONE)
-			{
-				parent.startInfo->selectedArmy[id][i].setCount(TextOperations::parseMetric<int>(text));
-				parent.onChange();
-				selectedArmyInput[i]->enable();
-			}
-			else
-				selectedArmyInput[i]->disable();
-		});
-	}
-
-	setHeroIcon();
-	setCreatureIcons();
-}
-
-void BattleOnlyModeHeroSelector::setHeroIcon()
-{
-	OBJECT_CONSTRUCTION;
-
-	if(!parent.startInfo->selectedHero[id])
-	{
-		heroImage = std::make_shared<CPicture>(drawBlackBox(Point(58, 64), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect"), id == 1 ? Colors::WHITE : parent.disabledColor), Point(6, 7));
-		heroLabel = std::make_shared<CLabel>(160, 16, FONT_SMALL, ETextAlignment::CENTER, id == 1 ? Colors::WHITE : parent.disabledColor, LIBRARY->generaltexth->translate("core.genrltxt.507"));
-		for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
-			primSkillsInput[i]->setText("0");
-	}
-	else
-	{
-		heroImage = std::make_shared<CPicture>(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("PortraitsLarge"), EImageBlitMode::COLORKEY)->getImage((*parent.startInfo->selectedHero[id]).toHeroType()->imageIndex), Point(6, 7));
-		heroLabel = std::make_shared<CLabel>(160, 16, FONT_SMALL, ETextAlignment::CENTER, id == 1 ? Colors::WHITE : parent.disabledColor, (*parent.startInfo->selectedHero[id]).toHeroType()->getNameTranslated());
-		for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
-			primSkillsInput[i]->setText(std::to_string(parent.startInfo->primSkillLevel[id][i]));
-	}
-
-	heroImage->addLClickCallback([this](){
-		auto allowedSet = LIBRARY->heroh->getDefaultAllowed();
-		std::vector<HeroTypeID> heroes(allowedSet.begin(), allowedSet.end());
-		std::sort(heroes.begin(), heroes.end(), [](auto a, auto b) {
-			auto heroA = a.toHeroType();
-			auto heroB = b.toHeroType();
-			if(heroA->heroClass->faction != heroB->heroClass->faction)
-				return heroA->heroClass->faction < heroB->heroClass->faction;
-			if(heroA->heroClass->getId() != heroB->heroClass->getId())
-				return heroA->heroClass->getId() < heroB->heroClass->getId();
-			return heroA->getNameTranslated() < heroB->getNameTranslated();
-		});
-
-		int selectedIndex = !parent.startInfo->selectedHero[id] ? 0 : (1 + std::distance(heroes.begin(), std::find_if(heroes.begin(), heroes.end(), [this](auto heroID) {
-			return heroID == (*parent.startInfo->selectedHero[id]);
-    	})));
-		
-		std::vector<std::string> texts;
-		std::vector<std::shared_ptr<IImage>> images;
-		// Add "no hero" option
-		texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507"));
-		images.push_back(nullptr);
-		for (const auto & h : heroes)
-		{
-			texts.push_back(h.toHeroType()->getNameTranslated());
-
-			auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("PortraitsSmall"), h.toHeroType()->imageIndex, 0, EImageBlitMode::OPAQUE);
-			image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST);
-			images.push_back(image);
-		}
-		auto window = std::make_shared<CObjectListWindow>(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), [this, heroes](int index){
-			if(index == 0)
-			{
-				parent.startInfo->selectedHero[id] = std::nullopt;
-				parent.onChange();
-				return;
-			}
-			index--;
-
-			parent.startInfo->selectedHero[id] = heroes[index];
-
-			for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
-				parent.startInfo->primSkillLevel[id][i] = 0;
-			parent.onChange();
-		}, selectedIndex, images, true, true);
-		window->onPopup = [heroes](int index) {
-			if(index == 0)
-				return;
-			index--;
-
-			ENGINE->windows().createAndPushWindow<CHeroOverview>(heroes.at(index));
-		};
-		ENGINE->windows().pushWindow(window);
-	});
-
-	heroImage->addRClickCallback([this](){
-		if(!parent.startInfo->selectedHero[id])
-			return;
-		
-		ENGINE->windows().createAndPushWindow<CHeroOverview>(parent.startInfo->selectedHero[id]->toHeroType()->getId());
-	});
-}
-
-void BattleOnlyModeHeroSelector::setCreatureIcons()
-{
-	OBJECT_CONSTRUCTION;
-
-	for(int i = 0; i < creatureImage.size(); i++)
-	{
-		if(parent.startInfo->selectedArmy[id][i].getId() == CreatureID::NONE)
-		{
-			creatureImage[i] = std::make_shared<CPicture>(drawBlackBox(Point(32, 32), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect"), id == 1 ? Colors::WHITE : parent.disabledColor), Point(6 + i * 36, 78));
-			selectedArmyInput[i]->disable();
-		}
-		else
-		{
-			auto unit = parent.startInfo->selectedArmy[id][i];
-			auto creatureID = unit.getId();
-			creatureImage[i] = std::make_shared<CPicture>(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL"), EImageBlitMode::COLORKEY)->getImage(LIBRARY->creh->objects.at(creatureID)->getIconIndex()), Point(6 + i * 36, 78));
-			selectedArmyInput[i]->setText(TextOperations::formatMetric(unit.getCount(), 3));
-			selectedArmyInput[i]->enable();
-		}
-
-		creatureImage[i]->addLClickCallback([this, i](){
-			auto allowedSet = LIBRARY->creh->getDefaultAllowed();
-			std::vector<CreatureID> creatures(allowedSet.begin(), allowedSet.end());
-			std::sort(creatures.begin(), creatures.end(), [](auto a, auto b) {
-				auto creatureA = a.toCreature();
-				auto creatureB = b.toCreature();
-				if(creatureA->getFactionID() != creatureB->getFactionID())
-					return creatureA->getFactionID() < creatureB->getFactionID();
-				if(creatureA->getLevel() != creatureB->getLevel())
-					return creatureA->getLevel() < creatureB->getLevel();
-				if(creatureA->upgrades.size() != creatureB->upgrades.size())
-					return creatureA->upgrades.size() > creatureB->upgrades.size();
-				return creatureA->getNameSingularTranslated() < creatureB->getNameSingularTranslated();
-			});
-
-			int selectedIndex = parent.startInfo->selectedArmy[id][i].getId() == CreatureID::NONE ? 0 : (1 + std::distance(creatures.begin(), std::find_if(creatures.begin(), creatures.end(), [this, i](auto creatureID) {
-				return creatureID == parent.startInfo->selectedArmy[id][i].getId();
-			})));
-			
-			std::vector<std::string> texts;
-			std::vector<std::shared_ptr<IImage>> images;
-			// Add "no creature" option
-			texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507"));
-			images.push_back(nullptr);
-			for (const auto & c : creatures)
-			{
-				texts.push_back(c.toCreature()->getNameSingularTranslated());
-
-				auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("CPRSMALL"), c.toCreature()->getIconIndex(), 0, EImageBlitMode::OPAQUE);
-				image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST);
-				images.push_back(image);
-			}
-			auto window = std::make_shared<CObjectListWindow>(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeCreatureSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeCreatureSelect"), [this, creatures, i](int index){
-				if(index == 0)
-				{
-					parent.startInfo->selectedArmy[id][i] = CStackBasicDescriptor(CreatureID::NONE, 1);
-					parent.onChange();
-					return;
-				}
-				index--;
-
-				auto creature = creatures.at(index).toCreature();
-				parent.startInfo->selectedArmy[id][i] = CStackBasicDescriptor(creature->getId(), 100);
-				parent.onChange();
-			}, selectedIndex, images, true, true);
-			window->onPopup = [creatures](int index) {
-				if(index == 0)
-					return;
-				index--;
-
-				ENGINE->windows().createAndPushWindow<CStackWindow>(creatures.at(index).toCreature(), true);
-			};
-			ENGINE->windows().pushWindow(window);
-		});
-
-		creatureImage[i]->addRClickCallback([this, i](){
-			if(parent.startInfo->selectedArmy[id][i].getId() == CreatureID::NONE)
-				return;
-			
-			ENGINE->windows().createAndPushWindow<CStackWindow>(LIBRARY->creh->objects.at(parent.startInfo->selectedArmy[id][i].getId()).get(), true);
-		});
-	}
-}
-
-void BattleOnlyModeWindow::startBattle()
-{
-	auto rng = &CRandomGenerator::getDefault();
-	
-	map->initTerrain();
-	map->getEditManager()->clearTerrain(rng);
-
-	map->getEditManager()->getTerrainSelection().selectAll();
-	map->getEditManager()->drawTerrain(!startInfo->selectedTerrain ? TerrainId::DIRT : *startInfo->selectedTerrain, 0, rng);
-
-	map->players[0].canComputerPlay = true;
-	map->players[0].canHumanPlay = true;
-	map->players[1] = map->players[0];
-
-	auto knownHeroes = LIBRARY->objtypeh->knownSubObjects(Obj::HERO);
-
-	auto addHero = [&, this](int sel, PlayerColor color, const int3 & position)
-	{
-		auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::HERO, (*startInfo->selectedHero[sel]).toHeroType()->heroClass->getId());
-		auto templates = factory->getTemplates();
-		auto obj = std::dynamic_pointer_cast<CGHeroInstance>(factory->create(cb.get(), templates.front()));
-		obj->setHeroType(*startInfo->selectedHero[sel]);
-
-		obj->setOwner(color);
-		obj->pos = position;
-		for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
-			obj->pushPrimSkill(PrimarySkill(i), startInfo->primSkillLevel[sel][i]);
-		obj->clearSlots();
-		for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++)
-			if(startInfo->selectedArmy[sel][slot].getId() != CreatureID::NONE)
-				obj->setCreature(SlotID(slot), startInfo->selectedArmy[sel][slot].getId(), startInfo->selectedArmy[sel][slot].getCount());
-		map->getEditManager()->insertObject(obj);
-	};
-
-	addHero(0, PlayerColor(0), int3(5, 6, 0));
-	if(!startInfo->selectedTown)
-		addHero(1, PlayerColor(1), int3(5, 5, 0));
-	else
-	{
-		auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::TOWN, *startInfo->selectedTown);
-		auto templates = factory->getTemplates();
-		auto obj = factory->create(cb.get(), templates.front());
-		auto townObj = std::dynamic_pointer_cast<CGTownInstance>(obj);
-		obj->setOwner(PlayerColor(1));
-		obj->pos = int3(5, 5, 0);
-		for (const auto & building : townObj->getTown()->getAllBuildings())
-			townObj->addBuilding(building);
-		if(!startInfo->selectedHero[1])
-		{
-			for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++)
-				if(startInfo->selectedArmy[1][slot].getId() != CreatureID::NONE)
-					townObj->getArmy()->setCreature(SlotID(slot), startInfo->selectedArmy[1][slot].getId(), startInfo->selectedArmy[1][slot].getCount());
-		}
-		else
-			addHero(1, PlayerColor(1), int3(5, 5, 0));
-
-		map->getEditManager()->insertObject(townObj);
-	}
-
-	auto path = VCMIDirs::get().userDataPath() / "Maps";
-	boost::filesystem::create_directories(path);
-	const std::string fileName = "BattleOnlyMode.vmap";
-	const auto fullPath = path / fileName;
-	CMapService mapService;
-	mapService.saveMap(map, fullPath);
-	CResourceHandler::get()->updateFilteredFiles([&](const std::string & mount) { return true; });
-
-	auto mapInfo = std::make_shared<CMapInfo>();
-	mapInfo->mapInit("Maps/BattleOnlyMode");
-	GAME->server().setMapInfo(mapInfo);
-	ExtraOptionsInfo extraOptions;
-	extraOptions.unlimitedReplay = true;
-	GAME->server().setExtraOptionsInfo(extraOptions);
-	GAME->server().sendStartGame();
-}

+ 975 - 0
client/lobby/BattleOnlyModeTab.cpp

@@ -0,0 +1,975 @@
+/*
+ * BattleOnlyModeTab.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "BattleOnlyModeTab.h"
+#include "CLobbyScreen.h"
+
+#include "../CServerHandler.h"
+#include "../GameEngine.h"
+#include "../GameInstance.h"
+
+#include "../render/IRenderHandler.h"
+#include "../render/CAnimation.h"
+#include "../render/Canvas.h"
+#include "../render/CanvasImage.h"
+#include "../render/IFont.h"
+#include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/TextControls.h"
+#include "../widgets/CTextInput.h"
+#include "../widgets/Images.h"
+#include "../widgets/CComponent.h"
+#include "../windows/GUIClasses.h"
+#include "../windows/CHeroOverview.h"
+#include "../windows/CCreatureWindow.h"
+#include "../windows/InfoWindows.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/GameLibrary.h"
+#include "../../lib/gameState/CGameState.h"
+#include "../../lib/networkPacks/PacksForLobby.h"
+#include "../../lib/StartInfo.h"
+#include "../../lib/VCMIDirs.h"
+#include "../../lib/CRandomGenerator.h"
+#include "../../lib/CSkillHandler.h"
+#include "../../lib/callback/EditorCallback.h"
+#include "../../lib/entities/hero/CHero.h"
+#include "../../lib/entities/hero/CHeroClass.h"
+#include "../../lib/entities/hero/CHeroHandler.h"
+#include "../../lib/entities/faction/CTown.h"
+#include "../../lib/entities/faction/CTownHandler.h"
+#include "../../lib/entities/artifact/CArtifact.h"
+#include "../../lib/entities/artifact/CArtHandler.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
+#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h"
+#include "../../lib/mapping/CMap.h"
+#include "../../lib/mapping/CMapInfo.h"
+#include "../../lib/mapping/CMapEditManager.h"
+#include "../../lib/mapping/CMapService.h"
+#include "../../lib/mapping/MapFormat.h"
+#include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/spells/SpellSchoolHandler.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
+#include "../../lib/texts/MetaString.h"
+#include "../../lib/texts/TextOperations.h"
+#include "../../lib/filesystem/Filesystem.h"
+#include "../../lib/serializer/JsonSerializer.h"
+#include "../../lib/serializer/JsonDeserializer.h"
+
+BattleOnlyModeTab::BattleOnlyModeTab()
+	: startInfo(std::make_shared<BattleOnlyModeStartInfo>())
+	, disabledColor(GAME->server().isHost() ? Colors::WHITE : Colors::ORANGE)
+	, boxColor(ColorRGBA(128, 128, 128))
+	, disabledBoxColor(GAME->server().isHost() ? boxColor : ColorRGBA(116, 92, 16))
+{
+	OBJECT_CONSTRUCTION;
+
+	try
+	{
+		JsonNode node = persistentStorage["battleModeSettings"];
+		if(!node.isNull())
+		{
+			node.setModScope(ModScope::scopeGame());
+			JsonDeserializer handler(nullptr, node);
+			startInfo->serializeJson(handler);
+		}
+	}
+	catch(std::exception & e)
+	{
+		logGlobal->error("Error loading saved battleModeSettings, received exception: %s", e.what());
+	}
+
+	init();
+
+	backgroundImage = std::make_shared<CPicture>(ImagePath::builtin("AdventureOptionsBackgroundClear"), 0, 6);
+	title = std::make_shared<CLabel>(220, 35, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode"));
+	subTitle = std::make_shared<CMultiLineLabel>(Rect(55, 40, 333, 40), FONT_SMALL, ETextAlignment::BOTTOMCENTER, Colors::WHITE, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSubTitle"));
+
+	battlefieldSelector = std::make_shared<CButton>(Point(57, 552), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ selectTerrain(); });
+	battlefieldSelector->block(GAME->server().isGuest());
+	buttonReset = std::make_shared<CButton>(Point(259, 552), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](){ reset(); });
+	buttonReset->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeReset"), EFonts::FONT_SMALL, Colors::WHITE);
+
+	heroSelector1 = std::make_shared<BattleOnlyModeHeroSelector>(0, *this, Point(55, 90));
+	heroSelector2 = std::make_shared<BattleOnlyModeHeroSelector>(1, *this, Point(55, 320));
+
+	heroSelector1->setInputEnabled(GAME->server().isHost());
+
+	onChange();
+}
+
+void BattleOnlyModeTab::reset()
+{
+	if(GAME->server().isHost())
+	{
+		startInfo->selectedTerrain = TerrainId::DIRT;
+		startInfo->selectedTown = FactionID::ANY;
+		startInfo->selectedHero[0] = HeroTypeID::NONE;
+		startInfo->selectedArmy[0].fill(CStackBasicDescriptor(CreatureID::NONE, 1));
+		startInfo->secSkillLevel[0].fill(std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE));
+		startInfo->artifacts[0].clear();
+		startInfo->spellBook[0] = true;
+		startInfo->warMachines[0] = false;
+		startInfo->spells[0].clear();
+		for(size_t i=0; i<GameConstants::ARMY_SIZE; i++)
+			heroSelector1->selectedArmyInput.at(i)->disable();
+		for(size_t i=0; i<8; i++)
+			heroSelector1->selectedSecSkillInput.at(i)->disable();
+	}
+	startInfo->selectedHero[1] = HeroTypeID::NONE;
+	startInfo->selectedArmy[1].fill(CStackBasicDescriptor(CreatureID::NONE, 1));
+	startInfo->secSkillLevel[1].fill(std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE));
+	startInfo->artifacts[1].clear();
+	startInfo->spellBook[1] = true;
+	startInfo->warMachines[1] = false;
+	startInfo->spells[1].clear();
+	for(size_t i=0; i<8; i++)
+		heroSelector2->selectedSecSkillInput.at(i)->disable();
+	onChange();
+}
+
+void BattleOnlyModeTab::selectTerrain()
+{
+	std::vector<std::string> texts;
+	std::vector<std::shared_ptr<IImage>> images;
+
+	std::vector<std::shared_ptr<TerrainType>> terrains;
+	std::copy_if(LIBRARY->terrainTypeHandler->objects.begin(), LIBRARY->terrainTypeHandler->objects.end(), std::back_inserter(terrains), [](auto terrain) { return terrain->isPassable(); });
+	for (const auto & terrain : terrains)
+	{
+		texts.push_back(terrain->getNameTranslated());
+
+		const auto & patterns = LIBRARY->terviewh->getTerrainViewPatterns(terrain->getId());
+		TerrainViewPattern pattern;
+		for(auto & p : patterns)
+			if(p[0].id == "n1")
+				pattern = p[0];
+		auto image = ENGINE->renderHandler().loadImage(terrain->tilesFilename, pattern.mapping[0].first, 0, EImageBlitMode::OPAQUE);
+		image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST);
+		images.push_back(image);
+	}
+
+	auto factions = LIBRARY->townh->getDefaultAllowed();
+	for (const auto & faction : factions)
+	{
+		texts.push_back(faction.toFaction()->getNameTranslated());
+
+		auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("ITPA"), faction.toFaction()->town->clientInfo.icons[true][false] + 2, 0, EImageBlitMode::OPAQUE);
+		image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST);
+		images.push_back(image);
+	}
+
+	ENGINE->windows().createAndPushWindow<CObjectListWindow>(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefield"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefieldSelect"), [this, terrains, factions](int index){
+		if(terrains.size() > index)
+		{
+			startInfo->selectedTerrain = terrains[index]->getId();
+			startInfo->selectedTown = FactionID::ANY;
+		}
+		else
+		{
+			startInfo->selectedTerrain = TerrainId::NONE;
+			auto it = std::next(factions.begin(), index - terrains.size());
+			if (it != factions.end())
+				startInfo->selectedTown = *it;
+		}
+		onChange();
+	}, (startInfo->selectedTerrain != TerrainId::NONE ? static_cast<int>(startInfo->selectedTerrain) : static_cast<int>(startInfo->selectedTown + terrains.size())), images, true, true);
+}
+
+void BattleOnlyModeTab::init()
+{
+	map = std::make_unique<CMap>(nullptr);
+	map->version = EMapFormat::VCMI;
+	map->creationDateTime = std::time(nullptr);
+	map->width = 10;
+	map->height = 10;
+	map->mapLevels = 1;
+	map->battleOnly = true;
+	map->name = MetaString::createFromTextID("vcmi.lobby.battleOnlyMode");
+
+	cb = std::make_unique<EditorCallback>(map.get());
+	map->cb = cb.get();
+}
+
+void BattleOnlyModeTab::onChange()
+{
+	GAME->server().setBattleOnlyModeStartInfo(startInfo);
+}
+
+void BattleOnlyModeTab::update()
+{
+	setTerrainButtonText();
+	setStartButtonEnabled();
+	
+	heroSelector1->setHeroIcon();
+	heroSelector1->setCreatureIcons();
+	heroSelector1->setSecSkillIcons();
+	heroSelector1->setArtifactIcons();
+	heroSelector1->spellBook->setSelectedSilent(startInfo->spellBook[0]);
+	heroSelector1->warMachines->setSelectedSilent(startInfo->warMachines[0]);
+	heroSelector2->setHeroIcon();
+	heroSelector2->setCreatureIcons();
+	heroSelector2->setSecSkillIcons();
+	heroSelector2->setArtifactIcons();
+	heroSelector2->spellBook->setSelectedSilent(startInfo->spellBook[1]);
+	heroSelector2->warMachines->setSelectedSilent(startInfo->warMachines[1]);
+	redraw();
+
+	JsonNode node;
+	JsonSerializer handler(nullptr, node);
+	startInfo->serializeJson(handler);
+	Settings storage = persistentStorage.write["battleModeSettings"];
+	storage->Struct() = node.Struct();
+}
+
+void BattleOnlyModeTab::applyStartInfo(std::shared_ptr<BattleOnlyModeStartInfo> si)
+{
+	startInfo = si;
+	update();
+}
+
+void BattleOnlyModeTab::setTerrainButtonText()
+{
+	battlefieldSelector->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefield") + ":   " + (startInfo->selectedTerrain != TerrainId::NONE ? startInfo->selectedTerrain.toEntity(LIBRARY)->getNameTranslated() : startInfo->selectedTown.toEntity(LIBRARY)->getNameTranslated()), EFonts::FONT_SMALL, disabledColor);
+}
+
+void BattleOnlyModeTab::setStartButtonEnabled()
+{
+	bool army2Empty = std::all_of(startInfo->selectedArmy[1].begin(), startInfo->selectedArmy[1].end(), [](const auto x) { return x.getId() == CreatureID::NONE; });
+
+	bool canStart = (startInfo->selectedTerrain != TerrainId::NONE || startInfo->selectedTown != FactionID::ANY);
+	canStart &= (startInfo->selectedHero[0] != HeroTypeID::NONE && ((startInfo->selectedHero[1] != HeroTypeID::NONE) || (startInfo->selectedTown != FactionID::ANY && !army2Empty)));
+	(static_cast<CLobbyScreen *>(parent))->buttonStart->block(!canStart || GAME->server().isGuest());
+}
+
+std::shared_ptr<IImage> drawBlackBox(Point size, std::string text, ColorRGBA color)
+{
+	auto image = ENGINE->renderHandler().createImage(size, CanvasScalingPolicy::AUTO);
+	Canvas canvas = image->getCanvas();
+	canvas.drawColor(Rect(0, 0, size.x, size.y), Colors::BLACK);
+
+	std::vector<std::string> lines;
+	boost::split(lines, text, boost::is_any_of("\n"));
+	int lineH = ENGINE->renderHandler().loadFont(FONT_TINY)->getLineHeight();
+	int totalH = lines.size() * lineH;
+	int startY = (size.y - totalH) / 2 + lineH / 2;
+
+	for (size_t i = 0; i < lines.size(); ++i)
+		canvas.drawText(Point(size.x / 2, startY + i * lineH), FONT_TINY, color, ETextAlignment::CENTER, lines[i]);
+
+	return image;
+}
+
+BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab& p, Point position)
+: parent(p)
+, id(id)
+{
+	OBJECT_CONSTRUCTION;
+
+	pos.x += position.x;
+	pos.y += position.y;
+
+	backgroundImage = std::make_shared<CPicture>(ImagePath::builtin("heroSlotsBlue"), Point(3, 4));
+
+	for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
+	{
+		auto image = std::make_shared<CAnimImage>(AnimationPath::builtin("PSKIL32"), i, 0, 78 + i * 36, 26);
+		primSkills.push_back(image);
+		primSkillsBorder.push_back(std::make_shared<GraphicalPrimitiveCanvas>(Rect(78 + i * 36, 26, 32, 32)));
+		primSkillsBorder.back()->addRectangle(Point(0, 0), Point(32, 32), ColorRGBA(44, 108, 255));
+		primSkillsInput.push_back(std::make_shared<CTextInput>(Rect(78 + i * 36, 58, 32, 16), EFonts::FONT_SMALL, ETextAlignment::CENTER, false));
+		primSkillsInput.back()->setColor(id == 1 ? Colors::WHITE : parent.disabledColor);
+		primSkillsInput.back()->setFilterNumber(0, 100);
+		primSkillsInput.back()->setText("0");
+		primSkillsInput.back()->setCallback([this, i, id](const std::string & text){
+			parent.startInfo->primSkillLevel[id][i] = std::stoi(primSkillsInput[i]->getText());
+			parent.onChange();
+		});
+	}
+
+	creatureImage.resize(GameConstants::ARMY_SIZE);
+	for(size_t i=0; i<GameConstants::ARMY_SIZE; i++)
+	{
+		selectedArmyInput.push_back(std::make_shared<CTextInput>(Rect(5 + i * 36, 113, 32, 16), EFonts::FONT_SMALL, ETextAlignment::CENTER, false));
+		selectedArmyInput.back()->setColor(id == 1 ? Colors::WHITE : parent.disabledColor);
+		selectedArmyInput.back()->setFilterNumber(0, 10000000, 3);
+		selectedArmyInput.back()->setText("1");
+		selectedArmyInput.back()->setCallback([this, i, id](const std::string & text){
+			if(parent.startInfo->selectedArmy[id][i].getId() != CreatureID::NONE)
+			{
+				parent.startInfo->selectedArmy[id][i].setCount(TextOperations::parseMetric<int>(text));
+				parent.onChange();
+				selectedArmyInput[i]->enable();
+			}
+			else
+				selectedArmyInput[i]->disable();
+		});
+	}
+
+	for(size_t i=0; i<8; i++)
+	{
+		bool isLeft = (i % 2 == 0);
+		int line = (i / 2);
+		Point textPos(261 + (isLeft ? 0 : 36), 41 + line * 54);
+
+		selectedSecSkillInput.push_back(std::make_shared<CTextInput>(Rect(textPos, Point(32, 16)), EFonts::FONT_SMALL, ETextAlignment::CENTER, false));
+		selectedSecSkillInput.back()->setColor(id == 1 ? Colors::WHITE : parent.disabledColor);
+		selectedSecSkillInput.back()->setFilterNumber(0, 3);
+		selectedSecSkillInput.back()->setText("3");
+		selectedSecSkillInput.back()->setCallback([this, i, id](const std::string & text){
+			if(parent.startInfo->secSkillLevel[id][i].second != MasteryLevel::NONE)
+			{
+				parent.startInfo->secSkillLevel[id][i].second = static_cast<MasteryLevel::Type>(std::stoi(text));
+				parent.onChange();
+				selectedSecSkillInput[i]->enable();
+			}
+			else
+				selectedSecSkillInput[i]->disable();
+		});
+	}
+	secSkillImage.resize(8);
+
+	artifactImage.resize(14);
+
+	auto tmpIcon = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), ArtifactID(ArtifactID::SPELLBOOK).toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE);
+	tmpIcon->scaleTo(Point(16, 16), EScalingAlgorithm::NEAREST);
+	addIcon.push_back(std::make_shared<CPicture>(tmpIcon, Point(220, 32)));
+	addIcon.back()->addLClickCallback([this](){ manageSpells(); });
+	addIcon.back()->addRClickCallback([this, id](){
+		std::vector<std::shared_ptr<CComponent>> comps;
+		for(auto & spell : parent.startInfo->spells[id])
+			comps.push_back(std::make_shared<CComponent>(ComponentType::SPELL, spell, std::nullopt, CComponent::ESize::large));
+		CRClickPopup::createAndPush(LIBRARY->generaltexth->translate("artifact.core.spellBook.name"), comps);
+	});
+
+	spellBook = std::make_shared<CToggleButton>(Point(235, 31), AnimationPath::builtin("lobby/checkboxSmall"), CButton::tooltip(), [this, id](bool enabled){
+		parent.startInfo->spellBook[id] = enabled;
+		parent.onChange();
+		redraw();
+	});
+	spellBook->setSelectedSilent(parent.startInfo->spellBook[id]);
+
+	tmpIcon = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), ArtifactID(ArtifactID::BALLISTA).toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE);
+	tmpIcon->scaleTo(Point(16, 16), EScalingAlgorithm::NEAREST);
+	addIcon.push_back(std::make_shared<CPicture>(tmpIcon, Point(220, 56)));
+	warMachines = std::make_shared<CToggleButton>(Point(235, 55), AnimationPath::builtin("lobby/checkboxSmall"), CButton::tooltip(), [this, id](bool enabled){
+		parent.startInfo->warMachines[id] = enabled;
+		parent.onChange();
+		redraw();
+	});
+	warMachines->setSelectedSilent(parent.startInfo->warMachines[id]);
+
+	setHeroIcon();
+	setCreatureIcons();
+	setSecSkillIcons();
+	setArtifactIcons();
+}
+
+void BattleOnlyModeHeroSelector::manageSpells()
+{
+	std::vector<std::shared_ptr<CComponent>> resComps;
+	for(auto & spellId : parent.startInfo->spells[id])
+		resComps.push_back(std::make_shared<CComponent>(ComponentType::SPELL, spellId, std::nullopt, CComponent::ESize::large));
+
+	std::vector<std::pair<AnimationPath, CFunctionList<void()>>> pom;
+	for(int i = 0; i < 3; i++)
+		pom.emplace_back(AnimationPath::builtin("settingsWindow/button80"), nullptr);
+
+	auto allowedSet = LIBRARY->spellh->getDefaultAllowed();
+	std::vector<SpellID> allSpells(allowedSet.begin(), allowedSet.end());
+	allSpells.erase(std::remove_if(allSpells.begin(), allSpells.end(), [](const SpellID& spell) {
+		return !spell.toSpell()->isCombat();
+	}), allSpells.end());
+	std::sort(allSpells.begin(), allSpells.end(), [](auto a, auto b) {
+		auto A = a.toSpell();
+		auto B = b.toSpell();
+		if(A->getLevel() != B->getLevel())
+			return A->getLevel() < B->getLevel();
+		for (const auto schoolId : LIBRARY->spellSchoolHandler->getAllObjects())
+		{
+			if(A->schools.count(schoolId) && !B->schools.count(schoolId))
+				return true;
+			if(!A->schools.count(schoolId) && B->schools.count(schoolId))
+				return false;
+		}
+		return TextOperations::compareLocalizedStrings(A->getNameTranslated(), B->getNameTranslated());
+	});
+
+	std::vector<SpellID> toAdd;
+	std::vector<SpellID> toRemove;
+	for (const auto& spell : allSpells)
+	{
+		bool inCurrent = std::find(parent.startInfo->spells[id].begin(), parent.startInfo->spells[id].end(), spell) != parent.startInfo->spells[id].end();
+		if (inCurrent)
+			toRemove.push_back(spell);
+		else
+			toAdd.push_back(spell);
+	}
+
+	auto openList = [this](std::vector<SpellID> list, bool add){
+		std::vector<std::string> texts;
+		std::vector<std::shared_ptr<IImage>> images;
+		for (const auto & s : list)
+		{
+			texts.push_back(s.toSpell()->getNameTranslated());
+
+			auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("SpellInt"), s.toSpell()->getIconIndex() + 1, 0, EImageBlitMode::OPAQUE);
+			image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST);
+			images.push_back(image);
+		}
+
+		std::string title = LIBRARY->generaltexth->translate(add ? "vcmi.lobby.battleOnlySpellAdd" : "vcmi.lobby.battleOnlySpellRemove");
+		auto window = std::make_shared<CObjectListWindow>(texts, nullptr, title, title, [this, list, add](int index){
+			auto & v = parent.startInfo->spells[id];
+			if(add)	
+				v.push_back(list[index]);
+			else
+				v.erase(std::remove(v.begin(), v.end(), list[index]), v.end());
+
+			parent.onChange();
+			manageSpells();
+		}, 0, images, true, true);
+		window->onPopup = [list](int index) {
+			std::shared_ptr<CComponent> comp = std::make_shared<CComponent>(ComponentType::SPELL, list[index]);
+			CRClickPopup::createAndPush(list[index].toSpell()->getDescriptionTranslated(0), CInfoWindow::TCompsInfo(1, comp));
+		};
+		ENGINE->windows().pushWindow(window);
+	};
+
+	auto temp = std::make_shared<CInfoWindow>(LIBRARY->generaltexth->translate(parent.startInfo->spells[id].size() ? "vcmi.lobby.battleOnlySpellSelectCurrent" : "vcmi.lobby.battleOnlySpellSelect"), PlayerColor(0), resComps, pom);
+	temp->buttons[0]->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("lobby/addChannel")));
+	temp->buttons[0]->addCallback([openList, toAdd](){ openList(toAdd, true); });
+	temp->buttons[0]->addPopupCallback([](){ CRClickPopup::createAndPush(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlySpellAdd")); });
+	temp->buttons[1]->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("lobby/removeChannel")));
+	temp->buttons[1]->addCallback([openList, toRemove](){ openList(toRemove, false); });
+	temp->buttons[1]->addPopupCallback([](){ CRClickPopup::createAndPush(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlySpellRemove")); });
+	temp->buttons[1]->setEnabled(parent.startInfo->spells[id].size());
+	temp->buttons[2]->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("spellResearch/close")));
+	temp->buttons[2]->addPopupCallback([](){ CRClickPopup::createAndPush(LIBRARY->generaltexth->translate("core.genrltxt.600")); });
+
+	ENGINE->windows().pushWindow(temp);
+}
+
+void BattleOnlyModeHeroSelector::selectHero()
+{		
+	auto allowedSet = LIBRARY->heroh->getDefaultAllowed();
+	std::vector<HeroTypeID> heroes(allowedSet.begin(), allowedSet.end());
+	std::sort(heroes.begin(), heroes.end(), [](auto a, auto b) {
+		auto heroA = a.toHeroType();
+		auto heroB = b.toHeroType();
+		if(heroA->heroClass->faction != heroB->heroClass->faction)
+			return heroA->heroClass->faction < heroB->heroClass->faction;
+		if(heroA->heroClass->getId() != heroB->heroClass->getId())
+			return heroA->heroClass->getId() < heroB->heroClass->getId();
+		return heroA->getNameTranslated() < heroB->getNameTranslated();
+	});
+
+	int selectedIndex = parent.startInfo->selectedHero[id] == HeroTypeID::NONE ? 0 : (1 + std::distance(heroes.begin(), std::find_if(heroes.begin(), heroes.end(), [this](auto heroID) {
+		return heroID == parent.startInfo->selectedHero[id];
+	})));
+	
+	std::vector<std::string> texts;
+	std::vector<std::shared_ptr<IImage>> images;
+	// Add "no hero" option
+	texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507"));
+	images.push_back(nullptr);
+	for (const auto & h : heroes)
+	{
+		texts.push_back(h.toHeroType()->getNameTranslated());
+
+		auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("PortraitsSmall"), h.toHeroType()->imageIndex, 0, EImageBlitMode::OPAQUE);
+		image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST);
+		images.push_back(image);
+	}
+	auto window = std::make_shared<CObjectListWindow>(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), [this, heroes](int index){
+		if(index == 0)
+		{
+			parent.startInfo->selectedHero[id] = HeroTypeID::NONE;
+			parent.onChange();
+			return;
+		}
+		index--;
+
+		parent.startInfo->selectedHero[id] = heroes[index];
+
+		for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
+			parent.startInfo->primSkillLevel[id][i] = heroes[index].toHeroType()->heroClass->primarySkillInitial[i];
+
+		for(size_t i=0; i<8; i++)
+			if(heroes[index].toHeroType()->secSkillsInit.size() > i)
+				parent.startInfo->secSkillLevel[id][i] = std::make_pair(heroes[index].toHeroType()->secSkillsInit[i].first, MasteryLevel::Type(heroes[index].toHeroType()->secSkillsInit[i].second));
+			else
+				parent.startInfo->secSkillLevel[id][i] = std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE);
+		
+		parent.startInfo->spellBook[id] = heroes[index].toHeroType()->haveSpellBook;
+
+		parent.onChange();
+	}, selectedIndex, images, true, true);
+	window->onPopup = [heroes](int index) {
+		if(index == 0)
+			return;
+		index--;
+
+		ENGINE->windows().createAndPushWindow<CHeroOverview>(heroes.at(index));
+	};
+	ENGINE->windows().pushWindow(window);
+}
+
+void BattleOnlyModeHeroSelector::setHeroIcon()
+{
+	OBJECT_CONSTRUCTION;
+
+	if(parent.startInfo->selectedHero[id] == HeroTypeID::NONE)
+	{
+		heroImage = std::make_shared<CPicture>(drawBlackBox(Point(58, 64), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelectHero"), id == 1 ? parent.boxColor : parent.disabledBoxColor), Point(6, 7));
+		heroLabel = std::make_shared<CLabel>(160, 16, FONT_SMALL, ETextAlignment::CENTER, id == 1 ? Colors::WHITE : parent.disabledColor, LIBRARY->generaltexth->translate("core.genrltxt.507"));
+		for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
+			primSkillsInput[i]->setText("0");
+	}
+	else
+	{
+		heroImage = std::make_shared<CPicture>(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("PortraitsLarge"), EImageBlitMode::COLORKEY)->getImage(parent.startInfo->selectedHero[id].toHeroType()->imageIndex), Point(6, 7));
+		heroLabel = std::make_shared<CLabel>(160, 16, FONT_SMALL, ETextAlignment::CENTER, id == 1 ? Colors::WHITE : parent.disabledColor, parent.startInfo->selectedHero[id].toHeroType()->getNameTranslated());
+		for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
+			primSkillsInput[i]->setText(std::to_string(parent.startInfo->primSkillLevel[id][i]));
+	}
+
+	heroImage->addLClickCallback([this](){ selectHero(); });
+
+	heroImage->addRClickCallback([this](){
+		if(parent.startInfo->selectedHero[id] == HeroTypeID::NONE)
+			return;
+		
+		ENGINE->windows().createAndPushWindow<CHeroOverview>(parent.startInfo->selectedHero[id].toHeroType()->getId());
+	});
+}
+
+void BattleOnlyModeHeroSelector::selectCreature(int slot)
+{
+	auto allowedSet = LIBRARY->creh->getDefaultAllowed();
+	std::vector<CreatureID> creatures(allowedSet.begin(), allowedSet.end());
+	std::sort(creatures.begin(), creatures.end(), [](auto a, auto b) {
+		auto creatureA = a.toCreature();
+		auto creatureB = b.toCreature();
+		if ((creatureA->getFactionID() == FactionID::NEUTRAL) != (creatureB->getFactionID() == FactionID::NEUTRAL))
+			return creatureA->getFactionID() != FactionID::NEUTRAL;
+		if(creatureA->getFactionID() != creatureB->getFactionID())
+			return creatureA->getFactionID() < creatureB->getFactionID();
+		if(creatureA->getLevel() != creatureB->getLevel())
+			return creatureA->getLevel() < creatureB->getLevel();
+		if(creatureA->upgrades.size() != creatureB->upgrades.size())
+			return creatureA->upgrades.size() > creatureB->upgrades.size();
+		return creatureA->getNameSingularTranslated() < creatureB->getNameSingularTranslated();
+	});
+
+	int selectedIndex = parent.startInfo->selectedArmy[id][slot].getId() == CreatureID::NONE ? 0 : (1 + std::distance(creatures.begin(), std::find_if(creatures.begin(), creatures.end(), [this, slot](auto creatureID) {
+		return creatureID == parent.startInfo->selectedArmy[id][slot].getId();
+	})));
+	
+	std::vector<std::string> texts;
+	std::vector<std::shared_ptr<IImage>> images;
+	// Add "no creature" option
+	texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507"));
+	images.push_back(nullptr);
+	for (const auto & c : creatures)
+	{
+		texts.push_back(c.toCreature()->getNameSingularTranslated());
+
+		auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("CPRSMALL"), c.toCreature()->getIconIndex(), 0, EImageBlitMode::OPAQUE);
+		image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST);
+		images.push_back(image);
+	}
+	auto window = std::make_shared<CObjectListWindow>(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeCreatureSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeCreatureSelect"), [this, creatures, slot](int index){
+		if(index == 0)
+		{
+			parent.startInfo->selectedArmy[id][slot] = CStackBasicDescriptor(CreatureID::NONE, 1);
+			parent.onChange();
+			return;
+		}
+		index--;
+
+		auto creature = creatures.at(index).toCreature();
+		parent.startInfo->selectedArmy[id][slot] = CStackBasicDescriptor(creature->getId(), 100);
+		parent.onChange();
+	}, selectedIndex, images, true, true);
+	window->onPopup = [creatures](int index) {
+		if(index == 0)
+			return;
+		index--;
+
+		ENGINE->windows().createAndPushWindow<CStackWindow>(creatures.at(index).toCreature(), true);
+	};
+	ENGINE->windows().pushWindow(window);
+}
+
+void BattleOnlyModeHeroSelector::setCreatureIcons()
+{
+	OBJECT_CONSTRUCTION;
+
+	for(int i = 0; i < creatureImage.size(); i++)
+	{
+		if(parent.startInfo->selectedArmy[id][i].getId() == CreatureID::NONE)
+		{
+			MetaString str;
+			str.appendTextID("vcmi.lobby.battleOnlyModeSelectUnit");
+			str.replaceNumber(i + 1);
+			creatureImage[i] = std::make_shared<CPicture>(drawBlackBox(Point(32, 32), str.toString(), id == 1 ? parent.boxColor : parent.disabledBoxColor), Point(6 + i * 36, 78));
+			selectedArmyInput[i]->disable();
+		}
+		else
+		{
+			auto unit = parent.startInfo->selectedArmy[id][i];
+			auto creatureID = unit.getId();
+			creatureImage[i] = std::make_shared<CPicture>(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL"), EImageBlitMode::COLORKEY)->getImage(LIBRARY->creh->objects.at(creatureID)->getIconIndex()), Point(6 + i * 36, 78));
+			selectedArmyInput[i]->setText(TextOperations::formatMetric(unit.getCount(), 3));
+			selectedArmyInput[i]->enable();
+		}
+
+		creatureImage[i]->addLClickCallback([this, i](){ selectCreature(i); });
+
+		creatureImage[i]->addRClickCallback([this, i](){
+			if(parent.startInfo->selectedArmy[id][i].getId() == CreatureID::NONE)
+				return;
+			
+			ENGINE->windows().createAndPushWindow<CStackWindow>(LIBRARY->creh->objects.at(parent.startInfo->selectedArmy[id][i].getId()).get(), true);
+		});
+	}
+}
+
+void BattleOnlyModeHeroSelector::selectSecSkill(int slot)
+{
+	auto allowedSet = LIBRARY->skillh->getDefaultAllowed();
+	std::vector<SecondarySkill> skills(allowedSet.begin(), allowedSet.end());
+	skills.erase( // remove already added skills from selection
+		std::remove_if(
+			skills.begin(),
+			skills.end(),
+			[this, slot](auto & skill) {
+				return std::any_of(
+					parent.startInfo->secSkillLevel[id].begin(), parent.startInfo->secSkillLevel[id].end(),
+					[&skill](auto & s) { return s.first == skill; }
+				) && parent.startInfo->secSkillLevel[id][slot].first != skill;
+			}
+		),
+		skills.end()
+	);
+	std::sort(skills.begin(), skills.end(), [](auto a, auto b) {
+		auto skillA = a.toSkill();
+		auto skillB = b.toSkill();
+		return skillA->getNameTranslated() < skillB->getNameTranslated();
+	});
+
+	int selectedIndex = parent.startInfo->secSkillLevel[id][slot].second == MasteryLevel::NONE ? 0 : (1 + std::distance(skills.begin(), std::find_if(skills.begin(), skills.end(), [this, slot](auto skillID) {
+		return skillID == parent.startInfo->secSkillLevel[id][slot].first;
+	})));
+	
+	std::vector<std::string> texts;
+	std::vector<std::shared_ptr<IImage>> images;
+	// Add "no skill" option
+	texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507"));
+	images.push_back(nullptr);
+	for (const auto & c : skills)
+	{
+		texts.push_back(c.toSkill()->getNameTranslated());
+
+		auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("SECSK32"), c.toSkill()->getIconIndex(0), 0, EImageBlitMode::OPAQUE);
+		image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST);
+		images.push_back(image);
+	}
+	auto window = std::make_shared<CObjectListWindow>(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSecSkillSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSecSkillSelect"), [this, skills, slot](int index){
+		if(index == 0)
+		{
+			parent.startInfo->secSkillLevel[id][slot] = std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE);
+			parent.onChange();
+			return;
+		}
+		index--;
+
+		auto skill = skills.at(index).toSkill();
+		parent.startInfo->secSkillLevel[id][slot] = std::make_pair(skill->getId(), MasteryLevel::EXPERT);
+		parent.onChange();
+	}, selectedIndex, images, true, true);
+	window->onPopup = [skills](int index) {
+		if(index == 0)
+			return;
+		index--;
+
+		auto skillId = skills.at(index);
+		std::shared_ptr<CComponent> comp = std::make_shared<CComponent>(ComponentType::SEC_SKILL, skillId, MasteryLevel::EXPERT);
+		CRClickPopup::createAndPush(skillId.toSkill()->getDescriptionTranslated(MasteryLevel::EXPERT), CInfoWindow::TCompsInfo(1, comp));
+	};
+	ENGINE->windows().pushWindow(window);
+}
+
+void BattleOnlyModeHeroSelector::setSecSkillIcons()
+{
+	OBJECT_CONSTRUCTION;
+
+	for(int i = 0; i < secSkillImage.size(); i++)
+	{
+		bool isLeft = (i % 2 == 0);
+		int line = (i / 2);
+		Point imgPos(261 + (isLeft ? 0 : 36), 7 + line * 54);
+		auto skillInfo = parent.startInfo->secSkillLevel[id][i];
+		if(skillInfo.second == MasteryLevel::NONE)
+		{
+			MetaString str;
+			str.appendTextID("vcmi.lobby.battleOnlyModeSelectSkill");
+			str.replaceNumber(i + 1);
+			secSkillImage[i] = std::make_shared<CPicture>(drawBlackBox(Point(32, 32), str.toString(), id == 1 ? parent.boxColor : parent.disabledBoxColor), imgPos);
+			selectedSecSkillInput[i]->disable();
+		}
+		else
+		{
+			secSkillImage[i] = std::make_shared<CPicture>(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("SECSK32"), EImageBlitMode::COLORKEY)->getImage(skillInfo.first.toSkill()->getIconIndex(skillInfo.second - 1)), imgPos);
+			selectedSecSkillInput[i]->setText(std::to_string(skillInfo.second));
+			selectedSecSkillInput[i]->enable();
+		}
+
+		secSkillImage[i]->addLClickCallback([this, i](){ selectSecSkill(i); });
+
+		secSkillImage[i]->addRClickCallback([this, i](){
+			auto skillId = parent.startInfo->secSkillLevel[id][i].first;
+			auto skillLevel = parent.startInfo->secSkillLevel[id][i].second;
+
+			if(skillLevel == MasteryLevel::NONE)
+				return;
+
+			std::shared_ptr<CComponent> comp = std::make_shared<CComponent>(ComponentType::SEC_SKILL, skillId, skillLevel);
+			CRClickPopup::createAndPush(skillId.toSkill()->getDescriptionTranslated(skillLevel), CInfoWindow::TCompsInfo(1, comp));
+		});
+	}
+}
+
+std::vector<ArtifactPosition> getArtPos()
+{
+	std::vector<ArtifactPosition> artPos = {
+		ArtifactPosition::HEAD, ArtifactPosition::SHOULDERS, ArtifactPosition::NECK, ArtifactPosition::RIGHT_HAND, ArtifactPosition::LEFT_HAND, ArtifactPosition::TORSO, ArtifactPosition::FEET,
+		ArtifactPosition::RIGHT_RING, ArtifactPosition::LEFT_RING, ArtifactPosition::MISC1, ArtifactPosition::MISC2, ArtifactPosition::MISC3, ArtifactPosition::MISC4, ArtifactPosition::MISC5
+	};
+	return artPos;
+}
+
+void BattleOnlyModeHeroSelector::selectArtifact(int slot, ArtifactID artifactId)
+{
+	auto artPos = getArtPos();
+
+	auto allowedSet = LIBRARY->arth->getDefaultAllowed();
+	std::vector<ArtifactID> artifacts(allowedSet.begin(), allowedSet.end());
+	artifacts.erase( // remove already added and not for that slot allowed artifacts from selection
+		std::remove_if(
+			artifacts.begin(),
+			artifacts.end(),
+			[this, slot, artPos](auto & artifact) {
+				auto possibleSlots = artifact.toArtifact()->getPossibleSlots();
+				std::vector<ArtifactPosition> allowedSlots;
+				if(possibleSlots.find(ArtBearer::HERO) != possibleSlots.end() && !possibleSlots.at(ArtBearer::HERO).empty())
+					allowedSlots = possibleSlots.at(ArtBearer::HERO);
+				
+				return (std::any_of(parent.startInfo->artifacts[id].begin(), parent.startInfo->artifacts[id].end(), [&artifact](auto & a) {return a.second == artifact;})
+					&& parent.startInfo->artifacts[id][artPos[slot]] != artifact)
+					|| !std::any_of(allowedSlots.begin(), allowedSlots.end(), [slot, artPos](auto & p){ return p == artPos[slot]; });
+			}
+		),
+		artifacts.end()
+	);
+	std::sort(artifacts.begin(), artifacts.end(), [](auto a, auto b) {
+		auto artifactA = a.toArtifact();
+		auto artifactB = b.toArtifact();
+		return artifactA->getNameTranslated() < artifactB->getNameTranslated();
+	});
+
+	int selectedIndex = artifactId == ArtifactID::NONE ? 0 : (1 + std::distance(artifacts.begin(), std::find_if(artifacts.begin(), artifacts.end(), [artifactId](auto artID) {
+		return artID == artifactId;
+	})));
+	
+	std::vector<std::string> texts;
+	std::vector<std::shared_ptr<IImage>> images;
+	// Add "no artifact" option
+	texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507"));
+	images.push_back(nullptr);
+	for (const auto & a : artifacts)
+	{
+		texts.push_back(a.toArtifact()->getNameTranslated());
+
+		auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), a.toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE);
+		image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST);
+		images.push_back(image);
+	}
+	auto window = std::make_shared<CObjectListWindow>(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeArtifactSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeArtifactSelect"), [this, artifacts, slot, artPos](int index){
+		if(index == 0)
+		{
+			parent.startInfo->artifacts[id][artPos[slot]] = ArtifactID::NONE;
+			parent.onChange();
+			return;
+		}
+		index--;
+
+		auto artifact = artifacts.at(index);
+		parent.startInfo->artifacts[id][artPos[slot]] = artifact;
+		parent.onChange();
+	}, selectedIndex, images, true, true);
+	window->onPopup = [artifacts](int index) {
+		if(index == 0)
+			return;
+		index--;
+
+		auto artifactId = artifacts.at(index);
+		std::shared_ptr<CComponent> comp = std::make_shared<CComponent>(ComponentType::ARTIFACT, artifactId);
+		CRClickPopup::createAndPush(artifactId.toArtifact()->getDescriptionTranslated(), CInfoWindow::TCompsInfo(1, comp));
+	};
+	ENGINE->windows().pushWindow(window);
+}
+
+void BattleOnlyModeHeroSelector::setArtifactIcons()
+{
+	OBJECT_CONSTRUCTION;
+
+	auto artPos = getArtPos();
+
+	for(int i = 0; i < artifactImage.size(); i++)
+	{
+		int xPos = i % 7;
+		int yPos = i / 7;
+		Point imgPos(6 + xPos * 36, 137 + yPos * 36);
+		auto artifactId = parent.startInfo->artifacts[id][artPos[i]];
+		if(artifactId == ArtifactID::NONE)
+		{
+			MetaString str;
+			str.appendTextID("vcmi.lobby.battleOnlyModeSelectArtifact");
+			str.replaceTextID("vcmi.lobby.battleOnlyModeSelectArtifact." + std::to_string(artPos[i]));
+			artifactImage[i] = std::make_shared<CPicture>(drawBlackBox(Point(32, 32), str.toString(), id == 1 ? parent.boxColor : parent.disabledBoxColor), imgPos);
+		}
+		else
+		{
+			auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), artifactId.toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE);
+			image->scaleTo(Point(32, 32), EScalingAlgorithm::NEAREST);
+			artifactImage[i] = std::make_shared<CPicture>(image, imgPos);
+		}
+			
+		artifactImage[i]->addLClickCallback([this, i, artifactId](){ selectArtifact(i, artifactId); });
+
+		artifactImage[i]->addRClickCallback([this, i, artPos](){
+			auto artId = parent.startInfo->artifacts[id][artPos[i]];
+			if(artId == ArtifactID::NONE)
+				return;
+
+			std::shared_ptr<CComponent> comp = std::make_shared<CComponent>(ComponentType::ARTIFACT, artId);
+			CRClickPopup::createAndPush(artId.toArtifact()->getDescriptionTranslated(), CInfoWindow::TCompsInfo(1, comp));
+		});
+	}
+}
+
+void BattleOnlyModeTab::startBattle()
+{
+	auto rng = &CRandomGenerator::getDefault();
+	
+	map->initTerrain();
+	map->getEditManager()->clearTerrain(rng);
+
+	map->getEditManager()->getTerrainSelection().selectAll();
+	map->getEditManager()->drawTerrain(startInfo->selectedTerrain == TerrainId::NONE ? TerrainId::DIRT : startInfo->selectedTerrain, 0, rng);
+
+	map->players[0].canComputerPlay = true;
+	map->players[0].canHumanPlay = true;
+	map->players[1] = map->players[0];
+
+	auto knownHeroes = LIBRARY->objtypeh->knownSubObjects(Obj::HERO);
+
+	auto addHero = [&, this](int sel, PlayerColor color, const int3 & position)
+	{
+		auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::HERO, startInfo->selectedHero[sel].toHeroType()->heroClass->getId());
+		auto templates = factory->getTemplates();
+		auto obj = std::dynamic_pointer_cast<CGHeroInstance>(factory->create(cb.get(), templates.front()));
+		obj->setHeroType(startInfo->selectedHero[sel]);
+
+		obj->setOwner(color);
+		obj->pos = position;
+		for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
+			obj->pushPrimSkill(PrimarySkill(i), startInfo->primSkillLevel[sel][i]);
+		obj->clearSlots();
+		for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++)
+			if(startInfo->selectedArmy[sel][slot].getId() != CreatureID::NONE && startInfo->selectedArmy[sel][slot].getCount() > 0)
+				obj->setCreature(SlotID(slot), startInfo->selectedArmy[sel][slot].getId(), startInfo->selectedArmy[sel][slot].getCount());
+
+		// give spellbook
+		if(!obj->getArt(ArtifactPosition::SPELLBOOK) && startInfo->spellBook[sel])
+			obj->putArtifact(ArtifactPosition::SPELLBOOK, map->createArtifact(ArtifactID::SPELLBOOK));
+		else if(obj->getArt(ArtifactPosition::SPELLBOOK) && !startInfo->spellBook[sel])
+			obj->removeArtifact(ArtifactPosition::SPELLBOOK);
+		
+		if(startInfo->warMachines[sel])
+		{
+			obj->putArtifact(ArtifactPosition::MACH1, map->createArtifact(ArtifactID::BALLISTA));
+			obj->putArtifact(ArtifactPosition::MACH2, map->createArtifact(ArtifactID::AMMO_CART));
+			obj->putArtifact(ArtifactPosition::MACH3, map->createArtifact(ArtifactID::FIRST_AID_TENT));
+		}
+		
+		for(const auto & spell : startInfo->spells[sel])
+			obj->addSpellToSpellbook(spell);
+
+		for(auto & artifact : startInfo->artifacts[sel])
+			if(artifact.second != ArtifactID::NONE)
+				obj->putArtifact(artifact.first, map->createArtifact(artifact.second));
+
+		for(const auto & skill : LIBRARY->skillh->objects) // reset all standard skills
+			obj->setSecSkillLevel(SecondarySkill(skill->getId()), MasteryLevel::NONE, ChangeValueMode::ABSOLUTE);
+		for(int skillSlot = 0; skillSlot < 8; skillSlot++)
+			obj->setSecSkillLevel(startInfo->secSkillLevel[sel][skillSlot].first, startInfo->secSkillLevel[sel][skillSlot].second, ChangeValueMode::ABSOLUTE);
+
+		map->getEditManager()->insertObject(obj);
+	};
+
+	addHero(0, PlayerColor(0), int3(5, 6, 0));
+	if(startInfo->selectedTown == FactionID::ANY)
+		addHero(1, PlayerColor(1), int3(5, 5, 0));
+	else
+	{
+		auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::TOWN, startInfo->selectedTown);
+		auto templates = factory->getTemplates();
+		auto obj = factory->create(cb.get(), templates.front());
+		auto townObj = std::dynamic_pointer_cast<CGTownInstance>(obj);
+		obj->setOwner(PlayerColor(1));
+		obj->pos = int3(5, 5, 0);
+		for (const auto & building : townObj->getTown()->getAllBuildings())
+			townObj->addBuilding(building);
+		if(startInfo->selectedHero[1] == HeroTypeID::NONE)
+		{
+			for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++)
+				if(startInfo->selectedArmy[1][slot].getId() != CreatureID::NONE && startInfo->selectedArmy[1][slot].getCount() > 0)
+					townObj->getArmy()->setCreature(SlotID(slot), startInfo->selectedArmy[1][slot].getId(), startInfo->selectedArmy[1][slot].getCount());
+		}
+		else
+			addHero(1, PlayerColor(1), int3(5, 5, 0));
+
+		map->getEditManager()->insertObject(townObj);
+	}
+
+	auto path = VCMIDirs::get().userDataPath() / "Maps";
+	boost::filesystem::create_directories(path);
+	const std::string fileName = "BattleOnlyMode.vmap";
+	const auto fullPath = path / fileName;
+	CMapService mapService;
+	mapService.saveMap(map, fullPath);
+	CResourceHandler::get()->updateFilteredFiles([&](const std::string & mount) { return true; });
+
+	auto mapInfo = std::make_shared<CMapInfo>();
+	mapInfo->mapInit("Maps/BattleOnlyMode");
+	GAME->server().setMapInfo(mapInfo);
+	ExtraOptionsInfo extraOptions;
+	extraOptions.unlimitedReplay = true;
+	GAME->server().setExtraOptionsInfo(extraOptions);
+	GAME->server().sendStartGame();
+}

+ 33 - 17
client/lobby/BattleOnlyMode.h → client/lobby/BattleOnlyModeTab.h

@@ -1,5 +1,5 @@
 /*
- * BattleOnlyMode.h, part of VCMI engine
+ * BattleOnlyModeTab.h, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *
@@ -11,6 +11,7 @@
 
 #include "../windows/CWindowObject.h"
 #include "../../lib/constants/EntityIdentifiers.h"
+#include "../../lib/mapping/CMap.h"
 
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -25,27 +26,32 @@ class FilledTexturePlayerColored;
 class CButton;
 class CPicture;
 class CLabel;
-class BattleOnlyModeWindow;
+class CMultiLineLabel;
+class BattleOnlyModeTab;
 class CAnimImage;
 class GraphicalPrimitiveCanvas;
 class CTextInput;
 class TransparentFilledRectangle;
-
-class BattleOnlyMode
-{
-public:
-	static void openBattleWindow();
-};
+class CToggleButton;
 
 class BattleOnlyModeHeroSelector : public CIntObject
 {
 private:
-	BattleOnlyModeWindow& parent;
+	BattleOnlyModeTab& parent;
 
 	std::shared_ptr<CPicture> backgroundImage;
 	std::shared_ptr<CPicture> heroImage;
 	std::shared_ptr<CLabel> heroLabel;
 	std::vector<std::shared_ptr<CPicture>> creatureImage;
+	std::vector<std::shared_ptr<CPicture>> secSkillImage;
+	std::vector<std::shared_ptr<CPicture>> artifactImage;
+
+	std::vector<std::shared_ptr<CPicture>> addIcon;
+
+	void selectHero();
+	void selectCreature(int slot);
+	void selectSecSkill(int slot);
+	void selectArtifact(int slot, ArtifactID artifactId);
 
 	int id;
 public:
@@ -54,13 +60,20 @@ public:
 	std::vector<std::shared_ptr<CTextInput>> primSkillsInput;
 
 	std::vector<std::shared_ptr<CTextInput>> selectedArmyInput;
+	std::vector<std::shared_ptr<CTextInput>> selectedSecSkillInput;
+
+	std::shared_ptr<CToggleButton> spellBook;
+	std::shared_ptr<CToggleButton> warMachines;
 
 	void setHeroIcon();
 	void setCreatureIcons();
-	BattleOnlyModeHeroSelector(int id, BattleOnlyModeWindow& parent, Point position);
+	void setSecSkillIcons();
+	void setArtifactIcons();
+	void manageSpells();
+	BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab& parent, Point position);
 };
 
-class BattleOnlyModeWindow : public CWindowObject
+class BattleOnlyModeTab : public CIntObject
 {
 	friend class BattleOnlyModeHeroSelector;
 private:
@@ -68,10 +81,9 @@ private:
 	std::unique_ptr<CMap> map;
 	std::shared_ptr<EditorCallback> cb;
 
-	std::shared_ptr<FilledTexturePlayerColored> backgroundTexture;
-	std::shared_ptr<CButton> buttonOk;
-	std::shared_ptr<CButton> buttonAbort;
+	std::shared_ptr<CPicture> backgroundImage;
 	std::shared_ptr<CLabel> title;
+	std::shared_ptr<CMultiLineLabel> subTitle;
 
 	std::shared_ptr<CButton> battlefieldSelector;
 	std::shared_ptr<CButton> buttonReset;
@@ -79,14 +91,18 @@ private:
 	std::shared_ptr<BattleOnlyModeHeroSelector> heroSelector2;
 
 	ColorRGBA disabledColor;
+	ColorRGBA boxColor;
+	ColorRGBA disabledBoxColor;
 
 	void init();
 	void onChange();
 	void update();
 	void setTerrainButtonText();
-	void setOkButtonEnabled();
-	void startBattle();
+	void selectTerrain();
+	void reset();
 public:
-	BattleOnlyModeWindow();
+	BattleOnlyModeTab();
 	void applyStartInfo(std::shared_ptr<BattleOnlyModeStartInfo> si);
+	void startBattle();
+	void setStartButtonEnabled();
 };

+ 43 - 5
client/lobby/CLobbyScreen.cpp

@@ -16,6 +16,7 @@
 #include "OptionsTab.h"
 #include "RandomMapTab.h"
 #include "SelectionTab.h"
+#include "BattleOnlyModeTab.h"
 
 #include "../CServerHandler.h"
 #include "../GameEngine.h"
@@ -87,7 +88,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType, bool hideScreen)
 
 		card->iconDifficulty->addCallback(std::bind(&IServerAPI::setDifficulty, &GAME->server(), _1));
 
-		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), LIBRARY->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_BEGIN_STANDARD_GAME);
+		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), LIBRARY->generaltexth->zelp[103], std::bind(&CLobbyScreen::start, this, false), EShortcut::LOBBY_BEGIN_STANDARD_GAME);
 		initLobby();
 		break;
 	}
@@ -96,13 +97,13 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType, bool hideScreen)
 		tabOpt = std::make_shared<OptionsTab>();
 		tabTurnOptions = std::make_shared<TurnOptionsTab>();
 		tabExtraOptions = std::make_shared<ExtraOptionsTab>();
-		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), LIBRARY->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_LOAD_GAME);
+		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), LIBRARY->generaltexth->zelp[103], std::bind(&CLobbyScreen::start, this, false), EShortcut::LOBBY_LOAD_GAME);
 		initLobby();
 		break;
 	}
 	case ESelectionScreen::campaignList:
 		tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, &GAME->server(), _1, nullptr);
-		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), EShortcut::LOBBY_BEGIN_CAMPAIGN);
+		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CButton::tooltip(), std::bind(&CLobbyScreen::start, this, true), EShortcut::LOBBY_BEGIN_CAMPAIGN);
 		break;
 	}
 
@@ -146,9 +147,33 @@ void CLobbyScreen::toggleTab(std::shared_ptr<CIntObject> tab)
 		GAME->server().sendGuiAction(LobbyGuiAction::OPEN_TURN_OPTIONS);
 	else if(tab == tabExtraOptions)
 		GAME->server().sendGuiAction(LobbyGuiAction::OPEN_EXTRA_OPTIONS);
+	else if(tab == tabBattleOnlyMode)
+		GAME->server().sendGuiAction(LobbyGuiAction::BATTLE_MODE);
+
+	if(tab == tabBattleOnlyMode)
+	{
+		tabBattleOnlyMode->setStartButtonEnabled();
+		card->clearSelection();
+	}
+	else
+	{
+		buttonStart->block(GAME->server().mi == nullptr || GAME->server().isGuest());
+		card->changeSelection();
+	}
+
 	CSelectionBase::toggleTab(tab);
 }
 
+void CLobbyScreen::start(bool campaign)
+{
+	if(curTab == tabBattleOnlyMode)
+		tabBattleOnlyMode->startBattle();
+	else if(campaign)
+		startCampaign();
+	else
+		startScenario(false);
+}
+
 void CLobbyScreen::startCampaign()
 {
 	if(!GAME->server().mi)
@@ -239,6 +264,16 @@ void CLobbyScreen::toggleChat()
 
 void CLobbyScreen::updateAfterStateChange()
 {
+	OBJECT_CONSTRUCTION;
+	if(!tabBattleOnlyMode)
+	{
+		tabBattleOnlyMode = std::make_shared<BattleOnlyModeTab>();
+		tabBattleOnlyMode->setEnabled(false);
+
+		if(GAME->server().battleMode)
+			toggleTab(tabBattleOnlyMode);
+	}
+
 	if(GAME->server().isHost() && screenType == ESelectionScreen::newGame)
 	{
 		bool isMultiplayer = GAME->server().loadMode == ELoadMode::MULTI;
@@ -259,9 +294,12 @@ void CLobbyScreen::updateAfterStateChange()
 			tabExtraOptions->recreate();
 	}
 
-	buttonStart->block(GAME->server().mi == nullptr || GAME->server().isGuest());
+	if(curTab && curTab != tabBattleOnlyMode)
+	{
+		buttonStart->block(GAME->server().mi == nullptr || GAME->server().isGuest());
+		card->changeSelection();
+	}
 
-	card->changeSelection();
 	if (card->iconDifficulty)
 	{
 		if (screenType == ESelectionScreen::loadGame)

+ 1 - 0
client/lobby/CLobbyScreen.h

@@ -23,6 +23,7 @@ public:
 	CLobbyScreen(ESelectionScreen type, bool hideScreen = false);
 	~CLobbyScreen();
 	void toggleTab(std::shared_ptr<CIntObject> tab) final;
+	void start(bool campaign);
 	void startCampaign();
 	void startScenario(bool allowOnlyAI = false);
 	void toggleMode(bool host);

+ 19 - 0
client/lobby/CSelectionBase.cpp

@@ -264,6 +264,7 @@ void InfoCard::changeSelection()
 	labelLossConditionText->setText(header->defeatMessage.toString());
 	flagbox->recreate();
 	labelDifficulty->setText(LIBRARY->generaltexth->arraytxt[142 + vstd::to_underlying(mapInfo->mapHeader->difficulty)]);
+	iconDifficulty->activate();
 	iconDifficulty->setSelected(SEL->getCurrentDifficulty());
 	if(SEL->screenType == ESelectionScreen::loadGame || SEL->screenType == ESelectionScreen::saveGame)
 		for(auto & button : iconDifficulty->buttons)
@@ -294,6 +295,24 @@ void InfoCard::changeSelection()
 	}
 }
 
+void InfoCard::clearSelection()
+{
+	labelSaveDate->setText("");
+	mapName->setText("");
+	mapDescription->setText("");
+
+	if(SEL->screenType == ESelectionScreen::campaignList)
+		return;
+
+	labelMapSize->setText("");
+
+	labelVictoryConditionText->setText("");
+	labelLossConditionText->setText("");
+	iconDifficulty->deactivate();
+	labelDifficulty->setText("");
+	labelDifficultyPercent->setText("");
+}
+
 void InfoCard::toggleChat()
 {
 	setChat(!showChat);

+ 3 - 0
client/lobby/CSelectionBase.h

@@ -29,6 +29,7 @@ class OptionsTab;
 class TurnOptionsTab;
 class ExtraOptionsTab;
 class SelectionTab;
+class BattleOnlyModeTab;
 class InfoCard;
 class CChatBox;
 class PvPBox;
@@ -78,6 +79,7 @@ public:
 	std::shared_ptr<TurnOptionsTab> tabTurnOptions;
 	std::shared_ptr<ExtraOptionsTab> tabExtraOptions;
 	std::shared_ptr<RandomMapTab> tabRand;
+	std::shared_ptr<BattleOnlyModeTab> tabBattleOnlyMode;
 	std::shared_ptr<CIntObject> curTab;
 
 	CSelectionBase(ESelectionScreen type);
@@ -127,6 +129,7 @@ public:
 	InfoCard();
 	void disableLabelRedraws();
 	void changeSelection();
+	void clearSelection();
 	void toggleChat();
 	void setChat(bool activateChat);
 };

+ 5 - 4
client/lobby/SelectionTab.cpp

@@ -12,7 +12,7 @@
 #include "SelectionTab.h"
 #include "CSelectionBase.h"
 #include "CLobbyScreen.h"
-#include "BattleOnlyMode.h"
+#include "BattleOnlyModeTab.h"
 
 #include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
@@ -244,9 +244,10 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 
 		if(tabType == ESelectionScreen::newGame)
 		{
-			buttonBattleOnlyMode = std::make_shared<CButton>(Point(23, 18), AnimationPath::builtin("lobby/battleButton"), CButton::tooltip("", LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode")), [tabTitle, tabTitleDelete](){
-				BattleOnlyMode::openBattleWindow();
-			});
+			buttonBattleOnlyMode = std::make_shared<CButton>(Point(23, 18), AnimationPath::builtin("lobby/battleButton"), CButton::tooltip("", LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode")), [this](){
+				auto lobby = static_cast<CLobbyScreen *>(parent);
+				lobby->toggleTab(lobby->tabBattleOnlyMode);
+			}, EShortcut::LOBBY_BATTLE_MODE);
 		}
 
 		if(tabType == ESelectionScreen::loadGame || tabType == ESelectionScreen::newGame)

+ 12 - 9
client/mainmenu/CMainMenu.cpp

@@ -144,7 +144,7 @@ static std::function<void()> genCommand(CMenuScreen * menu, std::vector<std::str
 {
 	static const std::vector<std::string> commandType = {"to", "campaigns", "start", "load", "exit", "highscores"};
 
-	static const std::vector<std::string> gameType = {"single", "multi", "campaign", "tutorial"};
+	static const std::vector<std::string> gameType = {"single", "multi", "campaign", "tutorial", "battle"};
 
 	std::list<std::string> commands;
 	boost::split(commands, string, boost::is_any_of("\t "));
@@ -174,13 +174,15 @@ static std::function<void()> genCommand(CMenuScreen * menu, std::vector<std::str
 				switch(std::find(gameType.begin(), gameType.end(), commands.front()) - gameType.begin())
 				{
 				case 0:
-					return []() { CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE); };
+					return []() { CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE, false); };
 				case 1:
 					return []() { ENGINE->windows().createAndPushWindow<CMultiMode>(ESelectionScreen::newGame); };
 				case 2:
-					return []() { CMainMenu::openLobby(ESelectionScreen::campaignList, true, {}, ELoadMode::NONE); };
+					return []() { CMainMenu::openLobby(ESelectionScreen::campaignList, true, {}, ELoadMode::NONE, false); };
 				case 3:
 					return []() { CMainMenu::startTutorial(); };
+				case 4:
+					return []() { CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE, true); };
 				}
 				break;
 			}
@@ -189,13 +191,13 @@ static std::function<void()> genCommand(CMenuScreen * menu, std::vector<std::str
 				switch(std::find(gameType.begin(), gameType.end(), commands.front()) - gameType.begin())
 				{
 				case 0:
-					return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::SINGLE); };
+					return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::SINGLE, false); };
 				case 1:
 					return []() { ENGINE->windows().createAndPushWindow<CMultiMode>(ESelectionScreen::loadGame); };
 				case 2:
-					return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::CAMPAIGN); };
+					return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::CAMPAIGN, false); };
 				case 3:
-					return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::TUTORIAL); };
+					return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::TUTORIAL, false); };
 
 				}
 			}
@@ -402,10 +404,11 @@ void CMainMenu::makeActiveInterface()
 	menu->switchToTab(menu->getActiveTab());
 }
 
-void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector<std::string> & names, ELoadMode loadMode)
+void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector<std::string> & names, ELoadMode loadMode, bool battleMode)
 {
 	GAME->server().resetStateForLobby(screenType == ESelectionScreen::newGame ? EStartMode::NEW_GAME : EStartMode::LOAD_GAME, screenType, EServerMode::LOCAL, names);
 	GAME->server().loadMode = loadMode;
+	GAME->server().battleMode = battleMode;
 
 	ENGINE->windows().createAndPushWindow<CSimpleJoinScreen>(host);
 }
@@ -448,7 +451,7 @@ void CMainMenu::startTutorial()
 		
 	auto mapInfo = std::make_shared<CMapInfo>();
 	mapInfo->mapInit(tutorialMap.getName());
-	CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE);
+	CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE, false);
 	GAME->server().startMapAfterConnection(mapInfo);
 }
 
@@ -615,7 +618,7 @@ void CMultiPlayers::enterSelectionScreen()
 		playerName->clear();
 	}
 
-	CMainMenu::openLobby(screenType, host, playerNames, loadMode);
+	CMainMenu::openLobby(screenType, host, playerNames, loadMode, false);
 }
 
 CSimpleJoinScreen::CSimpleJoinScreen(bool host)

+ 1 - 1
client/mainmenu/CMainMenu.h

@@ -154,7 +154,7 @@ public:
 	void activate() override;
 	void onScreenResize() override;
 	void makeActiveInterface();
-	static void openLobby(ESelectionScreen screenType, bool host, const std::vector<std::string> & names, ELoadMode loadMode);
+	static void openLobby(ESelectionScreen screenType, bool host, const std::vector<std::string> & names, ELoadMode loadMode, bool battleMode);
 	static void openCampaignLobby(const std::string & campaignFileName, std::string campaignSet = "");
 	static void openCampaignLobby(std::shared_ptr<CampaignState> campaign);
 	static void startTutorial();

+ 14 - 1
client/render/AssetGenerator.cpp

@@ -954,11 +954,24 @@ AssetGenerator::CanvasPtr AssetGenerator::createHeroSlotsColored(PlayerColor bac
 	static const std::array<ColorFilter, PlayerColor::PLAYER_LIMIT_I> filters = getColorFilters();
 	img->adjustPalette(filters[backColor.getNum()], 0);
 
-	auto image = ENGINE->renderHandler().createImage(Point(260, 150), CanvasScalingPolicy::IGNORE);
+	auto image = ENGINE->renderHandler().createImage(Point(327, 216), CanvasScalingPolicy::IGNORE);
 	Canvas canvas = image->getCanvas();
 	canvas.draw(img, Point(0, 0), Rect(3, 4, 253, 107));
 	for(int i = 0; i<7; i++)
 		canvas.draw(img, Point(1 + i * 36, 108), Rect(76, 57, 35, 17));
 
+	// sec skill
+	for(int x = 0; x<2; x++)
+		for(int y = 0; y<4; y++)
+		{
+			canvas.draw(img, Point(255 + x * 36, y * (36 + 18)), Rect(3, 75, 36, 36));
+			canvas.draw(img, Point(256 + x * 36, 37 + y * (36 + 18)), Rect(76, 57, 35, 17));
+		}
+	
+	// artifacts
+	for(int x = 0; x<7; x++)
+		for(int y = 0; y<2; y++)
+			canvas.draw(img, Point(x * 36, 130 + y * 36), Rect(3, 75, 36, 36));
+
 	return image;
 }

+ 15 - 0
client/renderSDL/RenderHandler.cpp

@@ -349,6 +349,20 @@ std::shared_ptr<IImage> RenderHandler::loadImage(const ImageLocator & locator)
 {
 	ImageLocator adjustedLocator = locator;
 
+	if(locator.image)
+	{
+		std::vector<std::string> splitted;
+		boost::split(splitted, (*locator.image).getOriginalName(), boost::is_any_of(":"));
+		if(splitted.size() == 3)
+		{
+			// allows image from def file with following filename (first group, then frame): "deffile.def:0:5"
+			adjustedLocator.defFile = AnimationPath::builtin(splitted[0]);
+			adjustedLocator.defGroup = std::stoi(splitted[1]);
+			adjustedLocator.defFrame = std::stoi(splitted[2]);
+			adjustedLocator.image = std::nullopt;
+		}
+	}
+
 	std::shared_ptr<ScalableImageInstance> result;
 
 	if (adjustedLocator.scalingFactor == 0)
@@ -389,6 +403,7 @@ std::shared_ptr<IImage> RenderHandler::loadImage(const ImagePath & path, EImageB
 	boost::split(splitted, name, boost::is_any_of(":"));
 	if(splitted.size() == 3)
 	{
+		// allows image from def file with following filename (first group, then frame): "deffile.def:0:5"
 		ImageLocator locator = getLocatorForAnimationFrame(AnimationPath::builtin(splitted[0]), std::stoi(splitted[2]), std::stoi(splitted[1]), 1, mode);
 		return loadImage(locator);
 	}

+ 1 - 1
client/renderSDL/ScalableImage.cpp

@@ -354,7 +354,7 @@ Rect ScalableImageInstance::contentRect() const
 Point ScalableImageInstance::dimensions() const
 {
 	if (scaledImage)
-		return scaledImage->dimensions() / ENGINE->screenHandler().getScalingFactor();
+		return scaledImage->dimensions();
 	return image->dimensions();
 }
 

+ 17 - 2
client/widgets/CComponent.cpp

@@ -96,14 +96,29 @@ void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optiona
 	if(Type == ComponentType::RESOURCE && !ValText.empty())
 		max = 80;
 
-	std::vector<std::string> textLines = CMessage::breakText(getSubtitle(), std::max<int>(max, pos.w), font);
 	const auto & fontPtr = ENGINE->renderHandler().loadFont(font);
+	{
+		std::string s = getSubtitle();
+
+		// remove color markup: "{color|"
+		s = std::regex_replace(s, std::regex("\\{[^|}]*\\|"), "");
+		// remove closing braces "}"
+		s.erase(std::remove(s.begin(), s.end(), '}'), s.end());
+
+		size_t longestWordLen = 0;
+		for(std::istringstream iss(s); iss >> s; )
+			longestWordLen = std::max(longestWordLen, fontPtr->getStringWidth(s));
+
+		max = std::max<int>(max, longestWordLen + 8);
+	}
+
+	std::vector<std::string> textLines = CMessage::breakText(getSubtitle(), std::max<int>(max, pos.w), font);
+
 	const int height = static_cast<int>(fontPtr->getLineHeight());
 
 	for(auto & line : textLines)
 	{
 		auto label = std::make_shared<CLabel>(pos.w/2, pos.h + height/2, font, ETextAlignment::CENTER, Colors::WHITE, line);
-
 		pos.h += height;
 		if(label->pos.w > pos.w)
 		{

+ 10 - 1
client/widgets/CTextInput.cpp

@@ -381,7 +381,16 @@ void CTextInput::numberFilter(std::string & text, const std::string & oldText, i
 	if (value < minValue)
 		text = metricDigits ? TextOperations::formatMetric(minValue, metricDigits) : std::to_string(minValue);
 	else if (value > maxValue)
-		text = metricDigits ? TextOperations::formatMetric(maxValue, metricDigits) : std::to_string(maxValue);
+	{
+		if(text.length() == 2 && oldText.length() == 1)
+		{
+			// special case: if max val < 10 you cannot erase number to add another -> in this case just use last typed number
+			int newNum = std::stoi(text.substr(text.size() - 1));
+			text = std::to_string(newNum < maxValue ? newNum : maxValue);
+		}
+		else
+			text = metricDigits ? TextOperations::formatMetric(maxValue, metricDigits) : std::to_string(maxValue);
+	}
 }
 
 void CTextInput::activate()

+ 4 - 0
client/widgets/Images.cpp

@@ -19,6 +19,7 @@
 #include "../render/Canvas.h"
 #include "../render/ColorFilter.h"
 #include "../render/Colors.h"
+#include "../eventsSDL/InputHandler.h"
 
 #include "../battle/BattleInterface.h"
 
@@ -150,7 +151,10 @@ void CPicture::addRClickCallback(const std::function<void()> & callback)
 void CPicture::clickPressed(const Point & cursorPosition)
 {
 	if(lCallback)
+	{
+		ENGINE->input().hapticFeedback();
 		lCallback();
+	}
 }
 
 void CPicture::showPopupWindow(const Point & cursorPosition)

+ 7 - 6
client/widgets/TextControls.cpp

@@ -351,12 +351,13 @@ Rect CMultiLineLabel::getTextLocation()
 
 	switch(alignment)
 	{
-	case ETextAlignment::TOPLEFT:     return Rect(pos.topLeft(), textSizeComputed);
-	case ETextAlignment::TOPCENTER:   return Rect(pos.topLeft(), textSizeComputed);
-	case ETextAlignment::CENTER:      return Rect(pos.topLeft() + textOffset / 2, textSizeComputed);
-	case ETextAlignment::CENTERLEFT:  return Rect(pos.topLeft() + Point(0, textOffset.y / 2), textSizeComputed);
-	case ETextAlignment::CENTERRIGHT: return Rect(pos.topLeft() + Point(textOffset.x, textOffset.y / 2), textSizeComputed);
-	case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSizeComputed);
+	case ETextAlignment::TOPLEFT:      return Rect(pos.topLeft(), textSizeComputed);
+	case ETextAlignment::TOPCENTER:    return Rect(pos.topLeft(), textSizeComputed);
+	case ETextAlignment::CENTER:       return Rect(pos.topLeft() + textOffset / 2, textSizeComputed);
+	case ETextAlignment::CENTERLEFT:   return Rect(pos.topLeft() + Point(0, textOffset.y / 2), textSizeComputed);
+	case ETextAlignment::CENTERRIGHT:  return Rect(pos.topLeft() + Point(textOffset.x, textOffset.y / 2), textSizeComputed);
+	case ETextAlignment::BOTTOMRIGHT:  return Rect(pos.topLeft() + textOffset, textSizeComputed);
+	case ETextAlignment::BOTTOMCENTER: return Rect(pos.topLeft() + Point(textOffset.x / 2, textOffset.y), textSizeComputed);
 	}
 	assert(0);
 	return Rect();

+ 6 - 3
client/windows/GUIClasses.cpp

@@ -1535,9 +1535,12 @@ CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::st
 	index(_id)
 {
 	OBJECT_CONSTRUCTION;
-	auto imgIndex = parent->itemsVisible[index].first;
-	if(parent->images.size() > index && parent->images[imgIndex])
-		icon = std::make_shared<CPicture>(parent->images[imgIndex], Point(1, 1));
+
+	auto it = std::find(parent->items.begin(), parent->items.end(), parent->itemsVisible[index]);
+	int imgIndex = (it != parent->items.end()) ? std::distance(parent->items.begin(), it) : -1;
+	if(imgIndex >= 0 && imgIndex < parent->images.size() && parent->images[imgIndex])
+		icon = std::make_shared<CPicture>(parent->images[imgIndex], Point(1,1));
+
 	border = std::make_shared<CPicture>(ImagePath::builtin("TPGATES"));
 	pos = border->pos;
 

+ 1 - 0
config/keyBindingsConfig.json

@@ -155,6 +155,7 @@
 		"lobbyToggleChat":          "C",
 		"lobbyTurnOptions":         "T",
 		"lobbyCampaignSets":        "G",
+		"lobbyBattleMode":          "X",
 		"mainMenuBack":             [ "B", "Escape" ],
 		"mainMenuCampaign":         "C",
 		"mainMenuCampaignAb":       "A",

+ 2 - 1
docs/players/Cheat_Codes.md

@@ -47,10 +47,11 @@ Gives specific creature in every slot, with optional amount. Examples:
 - `vcmiforgeofnoldorking` or `vcmiartifacts` - give all artifacts, except spell book, spell scrolls and war machines. Artifacts added via mods included  
 - `vcmiscrolls` - give spell scrolls for every possible spells
 
-### Movement points
+### Movement
 
 - `nwcnebuchadnezzar`, `nwcpodracer`, `nwccoconuts`, `vcminahar` or `vcmimove` - give unlimited (or specified amount of) movement points and free ship boarding
 - Alternative usage: `vcmimove <amount>` - gives specified amount of movement points
+- `vcmiteleport` - teleports hero to desired coordinate on map, usage: `vcmiteleport <x> <y> <z(optional)>`
 
 ### Resources
 

+ 0 - 15
lib/CStack.cpp

@@ -107,21 +107,6 @@ si32 CStack::magicResistance() const
 	return static_cast<si32>(100 - castChance);
 }
 
-BattleHex::EDir CStack::destShiftDir() const
-{
-	if(doubleWide())
-	{
-		if(side == BattleSide::ATTACKER)
-			return BattleHex::EDir::RIGHT;
-		else
-			return BattleHex::EDir::LEFT;
-	}
-	else
-	{
-		return BattleHex::EDir::NONE;
-	}
-}
-
 std::vector<SpellID> CStack::activeSpells() const
 {
 	std::vector<SpellID> ret;

+ 0 - 2
lib/CStack.h

@@ -66,8 +66,6 @@ public:
 	static BattleHexArray meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID);
 	static bool isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID);
 
-	BattleHex::EDir destShiftDir() const;
-
 	void prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const; //requires bsa.damageAmount filled
 	static void prepareAttacked(BattleStackAttacked & bsa,
 								vstd::RNG & rand,

+ 71 - 1
lib/StartInfo.cpp

@@ -21,6 +21,7 @@
 #include "mapping/CMapHeader.h"
 #include "mapping/CMapService.h"
 #include "modding/ModIncompatibility.h"
+#include "serializer/JsonSerializeFormat.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -243,11 +244,80 @@ TeamID LobbyInfo::getPlayerTeamId(const PlayerColor & color)
 
 BattleOnlyModeStartInfo::BattleOnlyModeStartInfo()
 	: selectedTerrain(TerrainId::DIRT)
-	, selectedTown(std::nullopt)
+	, selectedTown(FactionID::ANY)
 {
 	for(auto & element : primSkillLevel)
 		for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
 			element[i] = 0;
+	for(auto & element : warMachines)
+		element = false;
+	for(auto & element : spellBook)
+		element = true;
+}
+
+void BattleOnlyModeStartInfo::serializeJson(JsonSerializeFormat & handler)
+{
+	handler.serializeId("selectedTerrain", selectedTerrain);
+	handler.serializeId("selectedTown", selectedTown);
+	if(!handler.saving && selectedTown == FactionID::NONE)
+		selectedTown = FactionID::ANY;
+
+	auto slots = handler.enterArray("slots");
+	slots.resize(2);
+	for(int i = 0; i < 2; i++)
+	{
+		auto s = slots.enterStruct(i);
+		s->serializeId("selectedHero", selectedHero[i]);
+		{
+			auto selectedArmyArray = s->enterArray("selectedArmy");
+			selectedArmyArray.resize(GameConstants::ARMY_SIZE, JsonNode::JsonType::DATA_VECTOR);
+			for(int j = 0; j < GameConstants::ARMY_SIZE; j++)
+			{
+				JsonArraySerializer inner = selectedArmyArray.enterArray(j);
+				selectedArmy[i][j].serializeJson(handler);
+			}
+		}
+		{
+			auto primSkillLevelArray = s->enterArray("primSkillLevel");
+			primSkillLevelArray.resize(4, JsonNode::JsonType::DATA_VECTOR);
+			for(int j = 0; j < 4; j++)
+				primSkillLevelArray.serializeInt(j, primSkillLevel[i][j]);
+		}
+		{
+			auto secSkillLevelArray = s->enterArray("secSkillLevel");
+			secSkillLevelArray.resize(8, JsonNode::JsonType::DATA_VECTOR);
+			for(int j = 0; j < 8; j++)
+			{
+				JsonArraySerializer inner = secSkillLevelArray.enterArray(j);
+				inner->serializeId("skill", secSkillLevel[i][j].first);
+				inner->serializeEnum("masteryLevel", secSkillLevel[i][j].second, MasteryLevel::NONE, {"none", "basic", "advanced", "expert"});
+			}
+		}
+		if(handler.saving)
+		{
+			auto reversed = vstd::reverseMap(artifacts[i]);
+			std::map<ArtifactID, uint16_t> tmp;
+			for (auto& [id, pos] : reversed)
+    			tmp[id] = static_cast<uint16_t>(pos);
+			tmp.erase(ArtifactID::NONE);
+			s->serializeIdMap("artifacts", tmp);
+		}
+		else
+		{
+			std::map<ArtifactID, uint16_t> tmp;
+			s->serializeIdMap("artifacts", tmp);
+			std::map<ArtifactID, ArtifactPosition> converted;
+			for (auto &[id, pos] : tmp)
+				if(id != ArtifactID::NONE)
+					converted[id] = ArtifactPosition(pos);
+			artifacts[i] = vstd::reverseMap(converted);
+		}
+		s->serializeIdArray("spells", spells[i]);
+		if(!handler.saving)
+			spells[i].erase(std::remove(spells[i].begin(), spells[i].end(), SpellID::NONE), spells[i].end());
+		s->serializeBool("warMachines", warMachines[i]);
+		s->serializeBool("spellBook", spellBook[i]);
+	}
 }
 
 VCMI_LIB_NAMESPACE_END

+ 19 - 4
lib/StartInfo.h

@@ -243,16 +243,26 @@ struct DLL_LINKAGE LobbyInfo : public LobbyState
 class DLL_LINKAGE BattleOnlyModeStartInfo : public Serializeable
 {
 public:
-	std::optional<TerrainId> selectedTerrain;
-	std::optional<FactionID> selectedTown;
+	TerrainId selectedTerrain;
+	FactionID selectedTown;
 
-	std::array<std::optional<HeroTypeID>, 2> selectedHero;
-	std::array<std::array<CStackBasicDescriptor, 7>, 2> selectedArmy;
+	std::array<HeroTypeID, 2> selectedHero;
+	std::array<std::array<CStackBasicDescriptor, GameConstants::ARMY_SIZE>, 2> selectedArmy;
 
 	std::array<std::array<int, GameConstants::PRIMARY_SKILLS>, 2> primSkillLevel;
+	std::array<std::array<std::pair<SecondarySkill, MasteryLevel::Type>, 8>, 2> secSkillLevel;
+
+	std::array<std::map<ArtifactPosition, ArtifactID>, 2> artifacts;
+	std::array<std::vector<SpellID>, 2> spells;
+
+	std::array<bool, 2> warMachines;
+
+	std::array<bool, 2> spellBook;
 
 	BattleOnlyModeStartInfo();
 
+	void serializeJson(JsonSerializeFormat & handler);
+
 	template <typename Handler> void serialize(Handler &h)
 	{
 		h & selectedTerrain;
@@ -260,6 +270,11 @@ public:
 		h & selectedHero;
 		h & selectedArmy;
 		h & primSkillLevel;
+		h & secSkillLevel;
+		h & artifacts;
+		h & warMachines;
+		h & spellBook;
+		h & spells;
 	}
 };
 

+ 131 - 53
lib/battle/CBattleInfoCallback.cpp

@@ -616,91 +616,169 @@ BattleHexArray CBattleInfoCallback::battleGetAvailableHexes(const ReachabilityIn
 	return ret;
 }
 
-BattleHexArray CBattleInfoCallback::battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, BattleHexArray * attackable) const
+BattleHexArray CBattleInfoCallback::battleGetOccupiableHexes(const battle::Unit * unit, bool obtainMovementRange) const
 {
-	BattleHexArray ret = battleGetAvailableHexes(unit, obtainMovementRange);
-
-	if(ret.empty())
-		return ret;
-
-	if(addOccupiable && unit->doubleWide())
-	{
-		BattleHexArray occupiable;
+	return battleGetOccupiableHexes(battleGetAvailableHexes(unit, obtainMovementRange), unit);
+}
 
-		for(const auto & hex : ret)
-			occupiable.insert(unit->occupiedHex(hex));
+BattleHexArray CBattleInfoCallback::battleGetOccupiableHexes(const BattleHexArray & availableHexes, const battle::Unit * unit) const
+{
+	RETURN_IF_NOT_BATTLE(BattleHexArray());
+	if (!unit)
+		throw std::runtime_error("Undefined unit in battleGetOccupiableHexes!");
 
-		ret.insert(occupiable);
+	if (unit->doubleWide())
+	{ 
+		auto occupiableHexes = BattleHexArray(availableHexes);
+		for (auto hex : availableHexes)
+			occupiableHexes.insert(unit->occupiedHex(hex));
+		return occupiableHexes;
 	}
+	return availableHexes;
+}
+
+BattleHex CBattleInfoCallback::fromWhichHexAttack(const battle::Unit * attacker, const BattleHex & target, const BattleHex::EDir & direction) const
+{
+	RETURN_IF_NOT_BATTLE(BattleHex::INVALID);
+	if (!attacker)
+		throw std::runtime_error("Undefined attacker in fromWhichHexAttack!");
 
+	if (!target.isValid() || direction == BattleHex::NONE)
+		return BattleHex::INVALID;
 
-	if(attackable)
+	bool isAttacker = attacker->unitSide() == BattleSide::ATTACKER;
+	if (attacker->doubleWide())
 	{
-		auto meleeAttackable = [&](const BattleHex & hex) -> bool
-		{
-			// Return true if given hex has at least one available neighbour.
-			// Available hexes are already present in ret vector.
-			auto availableNeighbour = boost::find_if(ret, [=] (const BattleHex & availableHex)
-			{
-				return BattleHex::mutualPosition(hex, availableHex) >= 0;
-			});
-			return availableNeighbour != ret.end();
-		};
-		for(const auto * otherSt : battleAliveUnits(otherSide(unit->unitSide())))
+		// We need to find position of right hex of double-hex creature (or left for defending side)
+		// | TOP_LEFT | TOP_RIGHT |  RIGHT  |BOTTOM_RIGHT|BOTTOM_LEFT|  LEFT   |  TOP   | BOTTOM
+		// |  o o -   |    - o o  |  - -    |   - -      |    - -    |    - -  |  o o   |   - -
+		// |   - x -  |   - x -   | - x o o |  - x -     |   - x -   | o o x - | - x -  |  - x -
+		// |    - -   |    - -    |  - -    |   - o o    |  o o -    |    - -  |  - -   |   o o
+
+		switch (direction)
 		{
-			if(!otherSt->isValidTarget(false))
-				continue;
+			case BattleHex::TOP_LEFT:
+			case BattleHex::LEFT:
+			case BattleHex::BOTTOM_LEFT:
+				return target.cloneInDirection(direction, false)
+					.cloneInDirection(isAttacker ? BattleHex::NONE : BattleHex::LEFT, false);
+
+			case BattleHex::TOP_RIGHT:
+			case BattleHex::RIGHT:
+			case BattleHex::BOTTOM_RIGHT:
+				return target.cloneInDirection(direction, false)
+					.cloneInDirection(isAttacker ? BattleHex::RIGHT : BattleHex::NONE, false);
+
+			case BattleHex::TOP:
+				return target.cloneInDirection(isAttacker ? BattleHex::TOP_RIGHT : BattleHex::TOP_LEFT, false);
+
+			case BattleHex::BOTTOM:
+				return target.cloneInDirection(isAttacker ? BattleHex::BOTTOM_RIGHT : BattleHex::BOTTOM_LEFT, false);
+
+			default:
+				return BattleHex::INVALID;
+		}
+	}
+	if (direction == BattleHex::TOP || direction == BattleHex::BOTTOM)
+		return BattleHex::INVALID;
+	return target.cloneInDirection(direction, false);
+}
 
-			const BattleHexArray & occupied = otherSt->getHexes();
+BattleHex CBattleInfoCallback::toWhichHexMove(const battle::Unit * unit, const BattleHex & position) const
+{
+	return toWhichHexMove(battleGetAvailableHexes(unit, false), unit, position);
+}
 
-			if(battleCanShoot(unit, otherSt->getPosition()))
-			{
-				attackable->insert(occupied);
-				continue;
-			}
+BattleHex CBattleInfoCallback::toWhichHexMove(const BattleHexArray & availableHexes, const battle::Unit * unit, const BattleHex & position) const
+{
+	RETURN_IF_NOT_BATTLE(false);
 
-			for(const BattleHex & he : occupied)
-			{
-				if(meleeAttackable(he))
-					attackable->insert(he);
-			}
-		}
+	if (!unit)
+		throw std::runtime_error("Undefined unit in toWhichHexMove!");
+	if (!position.isValid())
+		return BattleHex::INVALID;
+
+	if (availableHexes.contains(position))
+		return position;
+	if (unit->doubleWide())
+	{
+		auto headPosition = position.cloneInDirection(unit->headDirection(), false);
+		if (availableHexes.contains(headPosition))
+			return headPosition;
 	}
+	return BattleHex::INVALID;
+}
 
-	return ret;
+bool CBattleInfoCallback::battleCanAttackHex(const battle::Unit * attacker, const BattleHex & position) const
+{
+	return battleCanAttackHex(battleGetAvailableHexes(attacker, false), attacker, position);
 }
 
-bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const battle::Unit * target, const BattleHex & dest) const
+bool CBattleInfoCallback::battleCanAttackHex(const BattleHexArray & availableHexes, const battle::Unit * attacker, const BattleHex & position) const
 {
-	RETURN_IF_NOT_BATTLE(false);
+	for (auto direction = 0; direction < 8; direction++)
+	{
+		if (battleCanAttackHex(availableHexes, attacker, position, BattleHex::EDir(direction)))
+			return true;
+	}
+	return false;
+}
 
-	if(battleTacticDist())
-		return false;
+bool CBattleInfoCallback::battleCanAttackHex(const battle::Unit * attacker, const BattleHex & position, const BattleHex::EDir & direction) const
+{
+	return battleCanAttackHex(battleGetAvailableHexes(attacker, false), attacker, position, direction);
+}
 
-	if (!stack || !target)
-		return false;
+bool CBattleInfoCallback::battleCanAttackHex(const BattleHexArray & availableHexes, const battle::Unit * attacker, const BattleHex & position, const BattleHex::EDir & direction) const
+{
+	RETURN_IF_NOT_BATTLE(false);
 
-	if(target->isInvincible())
-		return false;
+	if (!attacker)
+		throw std::runtime_error("Undefined attacker in battleCanAttackHex!");
 
-	if(!battleMatchOwner(stack, target))
+	if (!position.isValid() || direction == BattleHex::NONE)
 		return false;
 
-	if (!stack->isMeleeAttacker())
+	BattleHex fromHex = fromWhichHexAttack(attacker, position, direction);
+
+	//check if the attack is performed from an available hex
+	if (!fromHex.isValid() || !availableHexes.contains(fromHex))
 		return false;
 
-	if (stack->getPosition() != dest)
+	//if the movement ends in an obstacle, check if the obstacle allows attacking from that position
+	if (attacker->getPosition() != fromHex)
 	{
 		for (const auto & obstacle : battleGetAllObstacles())
 		{
-			if (obstacle->getStoppingTile().contains(dest))
+			if (obstacle->getStoppingTile().contains(fromHex))
 				return false;
-
-			if (stack->doubleWide() && obstacle->getStoppingTile().contains(stack->occupiedHex(dest)))
+			if (attacker->doubleWide() && obstacle->getStoppingTile().contains(attacker->occupiedHex(fromHex)))
 				return false;
 		}
 	}
 
+	return true;
+}
+
+bool CBattleInfoCallback::battleCanAttackUnit(const battle::Unit * attacker, const battle::Unit * target) const
+{
+	RETURN_IF_NOT_BATTLE(false);
+
+	if(battleTacticDist())
+		return false;
+
+	if (!attacker)
+		throw std::runtime_error("Undefined attacker in battleCanAttackUnit!");
+
+	if(!target || target->isInvincible())
+		return false;
+
+	if(attacker == target || !battleMatchOwner(attacker, target))
+		return false;
+
+	if (!attacker->isMeleeAttacker())
+		return false;
+
 	return target->alive();
 }
 

+ 17 - 5
lib/battle/CBattleInfoCallback.h

@@ -79,14 +79,26 @@ public:
 
 	void battleGetTurnOrder(std::vector<battle::Units> & out, const size_t maxUnits, const int maxTurns, const int turn = 0, BattleSide lastMoved = BattleSide::NONE) const;
 
-	///returns reachable hexes (valid movement destinations), DOES contain stack current position
-	BattleHexArray battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, BattleHexArray * attackable) const;
-
 	///returns reachable hexes (valid movement destinations), DOES contain stack current position (lite version)
 	BattleHexArray battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange) const;
-
 	BattleHexArray battleGetAvailableHexes(const ReachabilityInfo & cache, const battle::Unit * unit, bool obtainMovementRange) const;
 
+	//returns hexes the unit can occupy, obtainMovementRange ignores tactics mode (for double-wide units includes both head and tail)
+	BattleHexArray battleGetOccupiableHexes(const battle::Unit * unit, bool obtainMovementRange) const;
+	BattleHexArray battleGetOccupiableHexes(const BattleHexArray & availableHexes, const battle::Unit * unit) const;
+	//returns from which hex the attacker would attack the target from given direction; INVALID if not possible; the hex may be inccessible
+	BattleHex fromWhichHexAttack(const battle::Unit * attacker, const BattleHex & target, const BattleHex::EDir & direction) const;
+
+	//returns to which hex the (head of) unit would move to occupy position (possibly by tail)
+	BattleHex toWhichHexMove(const battle::Unit * unit, const BattleHex & position) const;
+	BattleHex toWhichHexMove(const BattleHexArray & availableHexes, const battle::Unit * unit, const BattleHex & position) const;
+
+	//return true iff attacker move towards and attack position from direction (spatial reasoning only)
+	bool battleCanAttackHex(const battle::Unit * attacker, const BattleHex & position, const BattleHex::EDir & direction) const;
+	bool battleCanAttackHex(const BattleHexArray & availableHexes, const battle::Unit * attacker, const BattleHex & position, const BattleHex::EDir & direction) const; //reuse availableHexes on multiple calls
+	bool battleCanAttackHex(const battle::Unit * attacker, const BattleHex & position) const; //check all directions
+	bool battleCanAttackHex(const BattleHexArray & availableHexes, const battle::Unit * attacker, const BattleHex & position) const; //reuse availableHexes on multiple calls
+
 	int battleGetSurrenderCost(const PlayerColor & Player) const; //returns cost of surrendering battle, -1 if surrendering is not possible
 	ReachabilityInfo::TDistances battleGetDistances(const battle::Unit * unit, const BattleHex & assumedPosition) const;
 	BattleHexArray battleGetAttackedHexes(const battle::Unit * attacker, const BattleHex & destinationTile, const BattleHex & attackerPos = BattleHex::INVALID) const;
@@ -96,7 +108,7 @@ public:
 	std::pair< BattleHexArray, int > getPath(const BattleHex & start, const BattleHex & dest, const battle::Unit * stack) const;
 
 	bool battleCanTargetEmptyHex(const battle::Unit * attacker) const; //determines of stack with given ID can target empty hex to attack - currently used only for SPELL_LIKE_ATTACK shooting
-	bool battleCanAttack(const battle::Unit * stack, const battle::Unit * target, const BattleHex & dest) const; //determines if stack with given ID can attack target at the selected destination
+	bool battleCanAttackUnit(const battle::Unit * attacker, const battle::Unit * target) const; //determines if attacker can attack target (no spatial reasoning)
 	bool battleCanShoot(const battle::Unit * attacker, const BattleHex & dest) const; //determines if stack with given ID shoot at the selected destination
 	bool battleCanShoot(const battle::Unit * attacker) const; //determines if stack with given ID shoot in principle
 	bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack

+ 16 - 3
lib/battle/Unit.cpp

@@ -91,9 +91,7 @@ BattleHexArray Unit::getAttackableHexes(const Unit * attacker) const
 			if (!coversPos(attacker->occupiedHex(attackOrigin)) && attackOrigin.isAvailable())
 				result.insert(attackOrigin);
 
-			bool isAttacker = attacker->unitSide() == BattleSide::ATTACKER;
-			BattleHex::EDir headDirection = isAttacker ? BattleHex::RIGHT : BattleHex::LEFT;
-			BattleHex headHex = attackOrigin.cloneInDirection(headDirection);
+			BattleHex headHex = attackOrigin.cloneInDirection(attacker->headDirection());
 
 			if (!coversPos(headHex) && headHex.isAvailable())
 				result.insert(headHex);
@@ -107,6 +105,21 @@ bool Unit::coversPos(const BattleHex & pos) const
 	return getPosition() == pos || (doubleWide() && (occupiedHex() == pos));
 }
 
+BattleHex::EDir Unit::headDirection() const
+{
+	if(doubleWide())
+	{
+		if(unitSide() == BattleSide::ATTACKER)
+			return BattleHex::EDir::RIGHT;
+		else
+			return BattleHex::EDir::LEFT;
+	}
+	else
+	{
+		return BattleHex::EDir::NONE;
+	}
+}
+
 const BattleHexArray & Unit::getHexes() const
 {
 	return getHexes(getPosition(), doubleWide(), unitSide());

+ 3 - 0
lib/battle/Unit.h

@@ -140,6 +140,9 @@ public:
 
 	bool coversPos(const BattleHex & position) const; //checks also if unit is double-wide
 
+	/// Returns the direction the double-wide unit is facing; returns NONE for single-hex units
+	BattleHex::EDir headDirection() const;
+
 	const BattleHexArray & getHexes() const; //up to two occupied hexes, starting from front
 	const BattleHexArray & getHexes(const BattleHex & assumedPos) const; //up to two occupied hexes, starting from front
 	static const BattleHexArray & getHexes(const BattleHex & assumedPos, bool twoHex, BattleSide side);

+ 2 - 9
lib/bonuses/Limiters.cpp

@@ -287,16 +287,9 @@ ILimiter::EDecision CreatureTerrainLimiter::limit(const BonusLimitationContext &
 	}
 	else
 	{
-		if (context.node.getNodeType() == BonusNodeType::STACK_BATTLE)
-		{
-			const auto * unit = dynamic_cast<const CStack *>(&context.node);
-			if (unit->getCurrentTerrain() == terrainType)
-				return ILimiter::EDecision::ACCEPT;
-		}
-		else
+		if(const auto * unit = retrieveStackInstance(&context.node))
 		{
-			const auto * unit = dynamic_cast<const CStackInstance*>(&context.node);
-			if (unit->getCurrentTerrain() == terrainType)
+			if (unit->getArmy() && unit->getCurrentTerrain() == terrainType)
 				return ILimiter::EDecision::ACCEPT;
 		}
 	}

+ 1 - 0
lib/callback/IGameEventsReceiver.h

@@ -41,6 +41,7 @@ public:
 	virtual void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what){}; //what: 1 - built, 2 - demolished
 
 	virtual void battleResultsApplied(){}; //called when all effects of last battle are applied
+	virtual void battleEnded(){}; //called when a battle has ended
 
 	virtual void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2){};
 

+ 7 - 0
lib/gameState/GameStatePackVisitor.cpp

@@ -1427,7 +1427,14 @@ void GameStatePackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack
 			hero->mana = std::min(hero->mana, currentBattle.getSide(i).initialMana);
 		}
 	}
+}
 
+void GameStatePackVisitor::visitBattleEnded(BattleEnded & pack)
+{
+	auto battleIter = boost::range::find_if(gs.currentBattles, [&](const auto & battle)
+	{
+		return battle->battleID == pack.battleID;
+	});
 	assert(battleIter != gs.currentBattles.end());
 	gs.currentBattles.erase(battleIter);
 }

+ 1 - 0
lib/gameState/GameStatePackVisitor.h

@@ -102,6 +102,7 @@ public:
 	void visitBattleNextRound(BattleNextRound & pack) override;
 	void visitBattleCancelled(BattleCancelled & pack) override;
 	void visitBattleResultsApplied(BattleResultsApplied & pack) override;
+	void visitBattleEnded(BattleEnded & pack) override;
 	void visitBattleResultAccepted(BattleResultAccepted & pack) override;
 	void visitTurnTimeUpdate(TurnTimeUpdate & pack) override;
 };

+ 1 - 0
lib/networkPacks/NetPackVisitor.h

@@ -113,6 +113,7 @@ public:
 	virtual void visitSetStackEffect(SetStackEffect & pack) {}
 	virtual void visitStacksInjured(StacksInjured & pack) {}
 	virtual void visitBattleResultsApplied(BattleResultsApplied & pack) {}
+	virtual void visitBattleEnded(BattleEnded & pack) {}
 	virtual void visitBattleObstaclesChanged(BattleObstaclesChanged & pack) {}
 	virtual void visitBattleSetStackProperty(BattleSetStackProperty & pack) {}
 	virtual void visitBattleTriggerEffect(BattleTriggerEffect & pack) {}

+ 5 - 0
lib/networkPacks/NetPacksLib.cpp

@@ -483,6 +483,11 @@ void BattleResultsApplied::visitTyped(ICPackVisitor & visitor)
 	visitor.visitBattleResultsApplied(*this);
 }
 
+void BattleEnded::visitTyped(ICPackVisitor & visitor)
+{
+	visitor.visitBattleEnded(*this);
+}
+
 void BattleObstaclesChanged::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitBattleObstaclesChanged(*this);

+ 16 - 0
lib/networkPacks/PacksForClientBattle.h

@@ -415,6 +415,22 @@ struct DLL_LINKAGE BattleResultsApplied : public CPackForClient
 	}
 };
 
+struct DLL_LINKAGE BattleEnded : public CPackForClient
+{
+	BattleID battleID = BattleID::NONE;
+	PlayerColor victor;
+	PlayerColor loser;
+	void visitTyped(ICPackVisitor & visitor) override;
+
+	template <typename Handler> void serialize(Handler & h)
+	{
+		h & battleID;
+		h & victor;
+		h & loser;
+		assert(battleID != BattleID::NONE);
+	}
+};
+
 struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient
 {
 	BattleID battleID = BattleID::NONE;

+ 1 - 0
lib/serializer/RegisterTypes.h

@@ -294,6 +294,7 @@ void registerTypes(Serializer &s)
 	s.template registerType<ChangeTownName>(252);
 	s.template registerType<SetTownName>(253);
 	s.template registerType<LobbySetBattleOnlyModeStartInfo>(254);
+	s.template registerType<BattleEnded>(255);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 23 - 23
lib/texts/Languages.h

@@ -90,29 +90,29 @@ inline const auto & getLanguageList()
 {
 	static const std::array<Options, 23> languages
 	{ {
-		{ "belarusian",  "Belarusian",  "Беларускі",    "CP1251",  "be_BY", "be", "bel", "%d.%m.%Y %H:%M",    EPluralForms::UK_3, true },
-		{ "bulgarian",   "Bulgarian",   "Български",    "CP1251",  "bg_BG", "bg", "bul", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true  },
-		{ "czech",       "Czech",       "Čeština",      "CP1250",  "cs_CZ", "cs", "cze", "%d.%m.%Y %H:%M",    EPluralForms::CZ_3, true },
-		{ "chinese",     "Chinese",     "简体中文",      "GBK",	   "zh_CN", "zh", "chi", "%Y-%m-%d %H:%M",    EPluralForms::VI_1, true }, // Note: actually Simplified Chinese
-		{ "english",     "English",     "English",      "CP1252",  "en_US", "en", "eng", "%Y-%m-%d %H:%M",    EPluralForms::EN_2, true }, // English uses international date/time format here
-		{ "finnish",     "Finnish",     "Suomi",        "CP1252",  "fi_FI", "fi", "fin", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true },
-		{ "french",      "French",      "Français",     "CP1252",  "fr_FR", "fr", "fre", "%d/%m/%Y %H:%M",    EPluralForms::FR_2, true },
-		{ "german",      "German",      "Deutsch",      "CP1252",  "de_DE", "de", "ger", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true },
-		{ "greek",       "Greek",       "ελληνικά",     "CP1253",  "el_GR", "el", "ell", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true },
-		{ "hungarian",   "Hungarian",   "Magyar",       "CP1250",  "hu_HU", "hu", "hun", "%Y. %m. %d. %H:%M", EPluralForms::EN_2, true },
-		{ "italian",     "Italian",     "Italiano",     "CP1250",  "it_IT", "it", "ita", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true },
-		{ "japanese",    "Japanese",    "日本語",        "JIS",     "ja_JP", "ja", "jpn", "%Y年%m月%d日 %H:%M", EPluralForms::VI_1, true },
-		{ "korean",      "Korean",      "한국어",	        "CP949",   "ko_KR", "ko", "kor", "%Y-%m-%d %H:%M",    EPluralForms::VI_1, true },
-		{ "polish",      "Polish",      "Polski",       "CP1250",  "pl_PL", "pl", "pol", "%d.%m.%Y %H:%M",    EPluralForms::PL_3, true },
-		{ "portuguese",  "Portuguese",  "Português",    "CP1252",  "pt_BR", "pt", "por", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
-		{ "romanian",    "Romanian",    "Română",       "CP28606", "ro_RO", "ro", "rum", "%Y-%m-%d %H:%M",    EPluralForms::RO_3, true },
-		{ "russian",     "Russian",     "Русский",      "CP1251",  "ru_RU", "ru", "rus", "%d.%m.%Y %H:%M",    EPluralForms::UK_3, true },
-		{ "spanish",     "Spanish",     "Español",      "CP1252",  "es_ES", "es", "spa", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true },
-		{ "swedish",     "Swedish",     "Svenska",      "CP1252",  "sv_SE", "sv", "swe", "%Y-%m-%d %H:%M",    EPluralForms::EN_2, true },
-		{ "norwegian",   "Norwegian",   "Norsk Bokmål", "UTF-8",   "nb_NO", "nb", "nor", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, false },
-		{ "turkish",     "Turkish",     "Türkçe",       "CP1254",  "tr_TR", "tr", "tur", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true },
-		{ "ukrainian",   "Ukrainian",   "Українська",   "CP1251",  "uk_UA", "uk", "ukr", "%d.%m.%Y %H:%M",    EPluralForms::UK_3, true },
-		{ "vietnamese",  "Vietnamese",  "Tiếng Việt",   "UTF-8",   "vi_VN", "vi", "vie", "%d/%m/%Y %H:%M",    EPluralForms::VI_1, true }, // Fan translation uses special encoding
+		{ "belarusian",  "Belarusian",  "Беларускі",    "CP1251",      "be_BY", "be", "bel", "%d.%m.%Y %H:%M",    EPluralForms::UK_3, true },
+		{ "bulgarian",   "Bulgarian",   "Български",    "CP1251",      "bg_BG", "bg", "bul", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true },
+		{ "czech",       "Czech",       "Čeština",      "CP1250",      "cs_CZ", "cs", "cze", "%d.%m.%Y %H:%M",    EPluralForms::CZ_3, true },
+		{ "chinese",     "Chinese",     "简体中文",      "GBK",         "zh_CN", "zh", "chi", "%Y-%m-%d %H:%M",    EPluralForms::VI_1, true }, // Note: actually Simplified Chinese
+		{ "english",     "English",     "English",      "CP1252",      "en_US", "en", "eng", "%Y-%m-%d %H:%M",    EPluralForms::EN_2, true }, // English uses international date/time format here
+		{ "finnish",     "Finnish",     "Suomi",        "CP1252",      "fi_FI", "fi", "fin", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true },
+		{ "french",      "French",      "Français",     "CP1252",      "fr_FR", "fr", "fre", "%d/%m/%Y %H:%M",    EPluralForms::FR_2, true },
+		{ "german",      "German",      "Deutsch",      "CP1252",      "de_DE", "de", "ger", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true },
+		{ "greek",       "Greek",       "ελληνικά",     "CP1253",      "el_GR", "el", "ell", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true },
+		{ "hungarian",   "Hungarian",   "Magyar",       "CP1250",      "hu_HU", "hu", "hun", "%Y. %m. %d. %H:%M", EPluralForms::EN_2, true },
+		{ "italian",     "Italian",     "Italiano",     "CP1250",      "it_IT", "it", "ita", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true },
+		{ "japanese",    "Japanese",    "日本語",        "JIS",         "ja_JP", "ja", "jpn", "%Y年%m月%d日 %H:%M", EPluralForms::VI_1, true },
+		{ "korean",      "Korean",      "한국어",        "CP949",       "ko_KR", "ko", "kor", "%Y-%m-%d %H:%M",    EPluralForms::VI_1, true },
+		{ "polish",      "Polish",      "Polski",       "CP1250",      "pl_PL", "pl", "pol", "%d.%m.%Y %H:%M",    EPluralForms::PL_3, true },
+		{ "portuguese",  "Portuguese",  "Português",    "CP1252",      "pt_BR", "pt", "por", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
+		{ "romanian",    "Romanian",    "Română",       "ISO-8859-16", "ro_RO", "ro", "rum", "%Y-%m-%d %H:%M",    EPluralForms::RO_3, true },
+		{ "russian",     "Russian",     "Русский",      "CP1251",      "ru_RU", "ru", "rus", "%d.%m.%Y %H:%M",    EPluralForms::UK_3, true },
+		{ "spanish",     "Spanish",     "Español",      "CP1252",      "es_ES", "es", "spa", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true },
+		{ "swedish",     "Swedish",     "Svenska",      "CP1252",      "sv_SE", "sv", "swe", "%Y-%m-%d %H:%M",    EPluralForms::EN_2, true },
+		{ "norwegian",   "Norwegian",   "Norsk Bokmål", "UTF-8",       "nb_NO", "nb", "nor", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, false },
+		{ "turkish",     "Turkish",     "Türkçe",       "CP1254",      "tr_TR", "tr", "tur", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true },
+		{ "ukrainian",   "Ukrainian",   "Українська",   "CP1251",      "uk_UA", "uk", "ukr", "%d.%m.%Y %H:%M",    EPluralForms::UK_3, true },
+		{ "vietnamese",  "Vietnamese",  "Tiếng Việt",   "UTF-8",       "vi_VN", "vi", "vie", "%d/%m/%Y %H:%M",    EPluralForms::VI_1, true }, // Fan translation uses special encoding
 	} };
 	static_assert(languages.size() == static_cast<size_t>(ELanguages::COUNT), "Languages array is missing a value!");
 

+ 1 - 1
server/battles/BattleActionProcessor.cpp

@@ -649,7 +649,7 @@ BattleActionProcessor::MovementResult BattleActionProcessor::moveStack(const CBa
 	//shifting destination (if we have double wide stack and we can occupy dest but not be exactly there)
 	if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack))
 	{
-		BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false);
+		BattleHex shifted = dest.cloneInDirection(curStack->headDirection(), false);
 
 		if(accessibility.accessible(shifted, curStack))
 			dest = shifted;

+ 13 - 3
server/battles/BattleFlowProcessor.cpp

@@ -473,9 +473,19 @@ bool BattleFlowProcessor::tryMakeAutomaticActionOfBallistaOrTowers(const CBattle
 			if (battle.battleCanShoot(unit))
 				return true;
 
-			BattleHexArray attackableHexes;
-			battle.battleGetAvailableHexes(unit, true, false, &attackableHexes);
-			return !attackableHexes.empty();
+			auto units = battle.battleAliveUnits();
+			auto availableHexes = battle.battleGetAvailableHexes(unit, true);
+
+			for (auto otherUnit : units)
+			{
+				if (battle.battleCanAttackUnit(unit, otherUnit))
+					for (auto position : otherUnit->getHexes())
+					{
+						if (battle.battleCanAttackHex(availableHexes, unit, position))
+							return true;
+					}
+			}
+			return false;
 		};
 
 		const auto & getTowerAttackValue = [&battle, &next] (const battle::Unit * unit)

+ 8 - 0
server/battles/BattleResultProcessor.cpp

@@ -558,6 +558,7 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
 	resultsApplied.battleID = battleID;
 	resultsApplied.victor = finishingBattle->victor;
 	resultsApplied.loser = finishingBattle->loser;
+	//BattleResultsApplied does not end the battle, it only applies most of its consequences
 	gameHandler->sendAndApply(resultsApplied);
 
 	// Remove beaten hero
@@ -612,6 +613,13 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
 		gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, loserHero);
 	}
 
+	//notify all players that battle has ended after all consequences are applied
+	BattleEnded ended;
+	ended.battleID = battleID;
+	ended.victor = finishingBattle->victor;
+	ended.loser = finishingBattle->loser;
+	gameHandler->sendAndApply(ended);
+
 	//handle victory/loss of engaged players
 	gameHandler->checkVictoryLossConditions({finishingBattle->loser, finishingBattle->victor});
 

+ 32 - 1
server/processors/PlayerMessageProcessor.cpp

@@ -736,6 +736,34 @@ void PlayerMessageProcessor::cheatSkill(PlayerColor player, const CGHeroInstance
 	gameHandler->changeSecSkill(hero, skill, mastery, ChangeValueMode::ABSOLUTE);
 }
 
+void PlayerMessageProcessor::cheatTeleport(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words)
+{
+	if (!hero)
+		return;
+
+	int3 pos;
+	try
+	{
+		pos.x = std::stoi(words.at(0));
+		pos.y = std::stoi(words.at(1));
+		pos.z = words.size() > 2 ? std::stoi(words.at(2)) : hero->pos.z;
+	}
+	catch(std::logic_error&)
+	{
+		return;
+	}
+
+	if(!gameHandler->gameState().getMap().isInTheMap(pos))
+		return;
+
+	auto destTile = gameHandler->gameState().getMap().getTile(pos);
+	auto heroTile = gameHandler->gameState().getMap().getTile(hero->pos);
+	if(!destTile.isClear(&heroTile))
+		return;
+
+	gameHandler->moveHero(hero->id, pos, EMovementMode::DIMENSION_DOOR);
+}
+
 bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerColor player, ObjectInstanceID currObj)
 {
 	std::vector<std::string> words;
@@ -779,7 +807,8 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo
 		                           "vcmimorale",      "nwcmorpheus",                                   "nwcmuchrejoicing",
 		                           "vcmigod",         "nwctheone",
 		                           "vcmiscrolls",
-		                           "vcmiskill"
+		                           "vcmiskill",
+		                           "vcmiteleport"
 	};
 
 	if(vstd::contains(localCheats, cheatName))
@@ -874,6 +903,7 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla
 	};
 	const auto & doCheatColorSchemeChange = [&](ColorScheme filter) { cheatColorSchemeChange(player, filter); };
 	const auto & doCheatSkill = [&]() { cheatSkill(player, hero, words); };
+	const auto & doCheatTeleport = [&]() { cheatTeleport(player, hero, words); };
 
 	std::map<std::string, std::function<void()>> callbacks = {
 		{"vcmiainur",               [&] () {doCheatGiveArmyFixed({ "archangel", "5" });}        },
@@ -957,6 +987,7 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla
 		{"nwcphisherprice",         [&] () {doCheatColorSchemeChange(ColorScheme::H2_SCHEME);}  },
 		{"vcmigray",                [&] () {doCheatColorSchemeChange(ColorScheme::GRAYSCALE);}  },
 		{"vcmiskill",               doCheatSkill                                                },
+		{"vcmiteleport",            doCheatTeleport                                             },
 	};
 
 	assert(callbacks.count(cheatName));

+ 1 - 0
server/processors/PlayerMessageProcessor.h

@@ -60,6 +60,7 @@ class PlayerMessageProcessor
 	void cheatMaxMorale(PlayerColor player, const CGHeroInstance * hero);
 	void cheatFly(PlayerColor player, const CGHeroInstance * hero);
 	void cheatSkill(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);
+	void cheatTeleport(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);
 
 	void commandExit(PlayerColor player, const std::vector<std::string> & words);
 	void commandKick(PlayerColor player, const std::vector<std::string> & words);

+ 1 - 0
test/vcai/mock_VCAI.h

@@ -78,6 +78,7 @@ public:
 	MOCK_METHOD1(heroManaPointsChanged, void(const CGHeroInstance * hero));
 	MOCK_METHOD3(heroSecondarySkillChanged, void(const CGHeroInstance * hero, int which, int val));
 	MOCK_METHOD0(battleResultsApplied, void());
+	MOCK_METHOD0(battleEnded, void());
 	MOCK_METHOD1(objectPropertyChanged, void(const SetObjectProperty * sop));
 	MOCK_METHOD3(buildChanged, void(const CGTownInstance * town, BuildingID buildingID, int what));
 	MOCK_METHOD3(heroBonusChanged, void(const CGHeroInstance * hero, const Bonus & bonus, bool gain));