Browse Source

battleonly gui

Laserlicht 1 month ago
parent
commit
ed562e42ba

+ 5 - 0
Mods/vcmi/Content/config/english.json

@@ -139,6 +139,11 @@
 	"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.battleOnlyModeBattleground" : "Battleground",
+	"vcmi.lobby.battleOnlyModeBattlegroundSelect" : "Select Battleground",
+	"vcmi.lobby.battleOnlyModeHeroSelect" : "Select Hero",
+	"vcmi.lobby.battleOnlyModeCreatureSelect" : "Select Creature",
+	"vcmi.lobby.battleOnlyModeSelect" : "Select",
 
 	"vcmi.broadcast.failedLoadGame" : "Failed to load game",
 	"vcmi.broadcast.command" : "Use '!help' to list available commands",

+ 5 - 0
Mods/vcmi/Content/config/german.json

@@ -139,6 +139,11 @@
 	"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.battleOnlyModeBattleground" : "Schlachtfeld",
+	"vcmi.lobby.battleOnlyModeBattlegroundSelect" : "Schlachtfeld auswählen",
+	"vcmi.lobby.battleOnlyModeHeroSelect" : "Helden auswählen",
+	"vcmi.lobby.battleOnlyModeCreatureSelect" : "Kreatur auswählen",
+	"vcmi.lobby.battleOnlyModeSelect" : "Wählen",
 
 	"vcmi.broadcast.failedLoadGame" : "Spiel konnte nicht geladen werden",
 	"vcmi.broadcast.command" : "Benutze '!help' um alle verfügbaren Befehle aufzulisten",

+ 3 - 0
client/battle/BattleWindow.cpp

@@ -51,6 +51,7 @@
 #include "../../lib/entities/artifact/CArtHandler.h"
 #include "../../lib/filesystem/ResourcePath.h"
 #include "../../lib/gameState/InfoAboutArmy.h"
+#include "../../lib/mapping/CMapHeader.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 
@@ -851,6 +852,8 @@ void BattleWindow::endWithAutocombat()
 
 void BattleWindow::showAll(Canvas & to)
 {
+	if(owner.curInt->cb->getMapHeader()->battleOnly)
+		to.fillTexture(ENGINE->renderHandler().loadImage(ImagePath::builtin("DiBoxBck"), EImageBlitMode::OPAQUE));
 	CIntObject::showAll(to);
 
 	if (ENGINE->screenDimensions().x != 800 || ENGINE->screenDimensions().y !=600)

+ 369 - 32
client/lobby/BattleOnlyMode.cpp

@@ -15,17 +15,34 @@
 #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/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"
@@ -33,6 +50,8 @@
 #include "../../lib/mapping/CMapEditManager.h"
 #include "../../lib/mapping/CMapService.h"
 #include "../../lib/mapping/MapFormat.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
+#include "../../lib/texts/TextOperations.h"
 #include "../../lib/filesystem/Filesystem.h"
 
 void BattleOnlyMode::openBattleWindow()
@@ -42,69 +61,361 @@ void BattleOnlyMode::openBattleWindow()
 
 BattleOnlyModeWindow::BattleOnlyModeWindow()
 	: CWindowObject(BORDERED)
+	, selectedTerrain(TerrainId::DIRT)
+	, selectedTown(FactionID::NONE)
 {
 	OBJECT_CONSTRUCTION;
 
-	pos.w = 640;
-	pos.h = 480;
+	pos.w = 700;
+	pos.h = 330;
 
 	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(183, 388), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ startBattle(); }, EShortcut::GLOBAL_ACCEPT);
-	buttonAbort = std::make_shared<CButton>(Point(250, 388), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ close(); }, EShortcut::GLOBAL_CANCEL);
+	buttonOk = std::make_shared<CButton>(Point(281, 288), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ startBattle(); }, EShortcut::GLOBAL_ACCEPT);
+	buttonAbort = std::make_shared<CButton>(Point(355, 288), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ close(); }, EShortcut::GLOBAL_CANCEL);
+	title = std::make_shared<CLabel>(350, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode"));
+
+	battlegroundSelector = std::make_shared<CButton>(Point(10, 294), 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)
+		{
+			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->objects;
+		factions.erase(std::remove_if(factions.begin(), factions.end(), [](const std::shared_ptr<CFaction>& n) {
+			return !n->town;
+		}), factions.end());
+		for (const auto & faction : factions)
+		{
+			texts.push_back(faction->getNameTranslated());
+
+			auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("ITPA"), faction->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.battleOnlyModeBattleground"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlegroundSelect"), [this, terrains, factions](int index){
+			if(terrains.size() > index)
+			{
+				selectedTerrain = terrains[index]->getId();
+				selectedTown = FactionID::NONE;
+			}
+			else
+			{
+				selectedTerrain = TerrainId::NONE;
+				selectedTown = factions[index - terrains.size()]->getId();
+			}
+			setTerrainButtonText();
+			setOkButtonEnabled();
+		}, (selectedTerrain != TerrainId::NONE ? static_cast<int>(selectedTerrain) : static_cast<int>(selectedTown + terrains.size())), images, true, true);
+	});
+
+	setTerrainButtonText();
+	setOkButtonEnabled();
+
+	heroSelector1 = std::make_shared<BattleOnlyModeHeroSelector>(*this, Point(0, 40));
+	heroSelector2 = std::make_shared<BattleOnlyModeHeroSelector>(*this, Point(0, 160));
 }
 
-void BattleOnlyModeWindow::startBattle()
+void BattleOnlyModeWindow::init()
 {
-	auto map = std::make_unique<CMap>(nullptr);
+	map = std::make_unique<CMap>(nullptr);
 	map->version = EMapFormat::VCMI;
 	map->creationDateTime = std::time(nullptr);
-	map->width = 4;
-	map->height = 3;
+	map->width = 10;
+	map->height = 10;
 	map->mapLevels = 1;
 	map->battleOnly = true;
+
+	cb = std::make_unique<EditorCallback>(map.get());
+}
+
+void BattleOnlyModeWindow::setTerrainButtonText()
+{
+	battlegroundSelector->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattleground") + ":   " + (selectedTerrain != TerrainId::NONE ? selectedTerrain.toEntity(LIBRARY)->getNameTranslated() : selectedTown.toEntity(LIBRARY)->getNameTranslated()), EFonts::FONT_SMALL, Colors::WHITE);
+}
+
+void BattleOnlyModeWindow::setOkButtonEnabled()
+{
+	bool canStart = (selectedTerrain != TerrainId::NONE || selectedTown != FactionID::NONE);
+	canStart &= (heroSelector1 && heroSelector1->selectedHero && ((heroSelector2 && heroSelector2->selectedHero) || (selectedTown != FactionID::NONE && heroSelector2->selectedArmy->stacksCount())));
+	buttonOk->block(!canStart);
+}
+
+std::shared_ptr<IImage> drawBlackBox(Point size, std::string text)
+{
+	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, Colors::WHITE, ETextAlignment::CENTER, text);
+	return image;
+}
+
+BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(BattleOnlyModeWindow& parent, Point position)
+: parent(parent)
+, selectedArmy(std::make_shared<CCreatureSet>())
+{
+	OBJECT_CONSTRUCTION;
+
+	pos.x += position.x;
+	pos.y += position.y;
+
+	backgroundImage = std::make_shared<CPicture>(ImagePath::builtin("heroSlotsBlue"), Point(0, 0));
+
+	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()->setFilterNumber(0, 100);
+		primSkillsInput.back()->setText("0");
+	}
+
+	creatureImage.resize(GameConstants::ARMY_SIZE);
+	creatureImageLabel.resize(GameConstants::ARMY_SIZE);
+
+	setHeroIcon();
+	setCreatureIcons();
+}
+
+void BattleOnlyModeHeroSelector::setHeroIcon()
+{
+	OBJECT_CONSTRUCTION;
+
+	if(!selectedHero)
+	{
+		heroImage = std::make_shared<CPicture>(drawBlackBox(Point(58, 64), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect")), Point(6, 7));
+		heroLabel = std::make_shared<CLabel>(160, 16, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->translate("core.genrltxt.507"));
+	}
+	else
+	{
+		heroImage = std::make_shared<CPicture>(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("PortraitsLarge"), EImageBlitMode::COLORKEY)->getImage(selectedHero->getHeroType()->imageIndex), Point(6, 7));
+		heroLabel = std::make_shared<CLabel>(160, 16, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, selectedHero->getNameTranslated());
+	}
+
+	heroImage->addLClickCallback([this](){
+		auto heroes = LIBRARY->heroh->objects;
+		std::sort(heroes.begin(), heroes.end(), [](const std::shared_ptr<CHero>& a, const std::shared_ptr<CHero>& b) {
+			if (a->heroClass->getId() == b->heroClass->getId())
+				return a->getNameTranslated() < b->getNameTranslated();
+			if (a->heroClass->faction == b->heroClass->faction)
+				return a->heroClass->getId() < b->heroClass->getId();
+			return a->heroClass->faction < b->heroClass->faction;
+		});
+		heroes.erase(std::remove_if(heroes.begin(), heroes.end(), [](const std::shared_ptr<CHero>& n) {
+			return n->special;
+		}), heroes.end());
+
+		int selectedIndex = !selectedHero ? 0 : (1 + std::distance(heroes.begin(), std::find_if(heroes.begin(), heroes.end(), [this](const std::shared_ptr<CHero>& heroPtr) {
+			return heroPtr.get() == selectedHero->getHeroType();
+    	})));
+		
+		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->getNameTranslated());
+
+			auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("PortraitsSmall"), h->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("object.core.hero.name"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), [this, heroes](int index){
+			if(index == 0)
+			{
+				selectedHero.reset();
+				setHeroIcon();
+				return;
+			}
+			index--;
+			auto hero = heroes[index];
+
+			auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::HERO, hero->heroClass->getId());
+			auto templates = factory->getTemplates();
+			auto obj = std::dynamic_pointer_cast<CGHeroInstance>(factory->create(parent.cb.get(), templates.front()));
+			obj->setHeroType(hero->getId());
+
+			selectedHero = obj;
+			setHeroIcon();
+			parent.setOkButtonEnabled();
+		}, selectedIndex, images, true, true);
+		window->onPopup = [this, heroes](int index) {
+			if(index == 0)
+				return;
+			index--;
+
+			ENGINE->windows().createAndPushWindow<CHeroOverview>(heroes[index]->getId());
+		};
+		ENGINE->windows().pushWindow(window);
+	});
+
+	heroImage->addRClickCallback([this](){
+		if(!selectedHero)
+			return;
+		
+		ENGINE->windows().createAndPushWindow<CHeroOverview>(selectedHero->getHeroType()->getId());
+	});
+}
+
+void BattleOnlyModeHeroSelector::setCreatureIcons()
+{
+	OBJECT_CONSTRUCTION;
+
+	for(int i = 0; i < creatureImage.size(); i++)
+	{
+		if(selectedArmy->slotEmpty(SlotID(i)))
+		{
+			creatureImage[i] = std::make_shared<CPicture>(drawBlackBox(Point(32, 32), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect")), Point(6 + i * 36, 78));
+			creatureImageLabel[i].reset();
+		}
+		else
+		{
+			auto creatureID = selectedArmy->Slots().at(SlotID(i))->getCreatureID();
+			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));
+			creatureImageLabel[i] = std::make_shared<CLabel>(36 + i * 36, 112, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, TextOperations::formatMetric(selectedArmy->Slots().at(SlotID(i))->getCount(), 4));
+		}
+
+		creatureImage[i]->addLClickCallback([this, i](){
+			auto creatures = LIBRARY->creh->objects;
+			std::sort(creatures.begin(), creatures.end(), [](const std::shared_ptr<CCreature>& a, const std::shared_ptr<CCreature>& b) {
+				if (a->getLevel() == b->getLevel())
+					return a->getNameSingularTranslated() < b->getNameSingularTranslated();
+				if (a->getFactionID() == b->getFactionID())
+					return a->getLevel() < b->getLevel();
+				return a->getFactionID() < b->getFactionID();
+			});
+			creatures.erase(std::remove_if(creatures.begin(), creatures.end(), [](const std::shared_ptr<CCreature>& n) {
+				return n->special;
+			}), creatures.end());
+
+			int selectedIndex = selectedArmy->slotEmpty(SlotID(i)) ? 0 : (1 + std::distance(creatures.begin(), std::find_if(creatures.begin(), creatures.end(), [this, i](const std::shared_ptr<CCreature>& creaturePtr) {
+				return creaturePtr->getId() == selectedArmy->Slots().at(SlotID(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->getNameSingularTranslated());
+
+				auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("CPRSMALL"), c->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("core.genrltxt.42"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeCreatureSelect"), [this, creatures, i](int index){
+				if(index == 0)
+				{
+					if(!selectedArmy->slotEmpty(SlotID(i)))
+						selectedArmy->eraseStack(SlotID(i));
+					setCreatureIcons();
+					return;
+				}
+				index--;
+				auto creature = creatures[index];
+				int amount = selectedArmy->slotEmpty(SlotID(i)) ? 1 : selectedArmy->Slots().at(SlotID(i))->getCount();
+				ENGINE->windows().createAndPushWindow<NumberInputWindow>(amount, [this, creature, i](int quantity){
+					selectedArmy->setCreature(SlotID(i), creature->getId(), quantity);
+					setCreatureIcons();
+				});
+			}, selectedIndex, images, true, true);
+			window->onPopup = [this, creatures](int index) {
+				if(index == 0)
+					return;
+				index--;
+
+				ENGINE->windows().createAndPushWindow<CStackWindow>(creatures.at(index).get(), true);
+			};
+			ENGINE->windows().pushWindow(window);
+		});
+
+		creatureImage[i]->addRClickCallback([this, i](){
+			if(selectedArmy->slotEmpty(SlotID(i)))
+				return;
+			
+			ENGINE->windows().createAndPushWindow<CStackWindow>(LIBRARY->creh->objects.at(selectedArmy->Slots().at(SlotID(i))->getCreatureID()).get(), true);
+		});
+	}
+}
+
+void BattleOnlyModeWindow::startBattle()
+{
+	auto rng = &CRandomGenerator::getDefault();
 	
 	map->initTerrain();
-	map->getEditManager()->clearTerrain(&CRandomGenerator::getDefault());
+	map->getEditManager()->clearTerrain(rng);
 
-	//auto terrain = LIBRARY->terrainTypeHandler->objects;
 	map->getEditManager()->getTerrainSelection().selectAll();
-	map->getEditManager()->drawTerrain(TerrainId::GRASS, 0, &CRandomGenerator::getDefault());
+	map->getEditManager()->drawTerrain(selectedTerrain == TerrainId::NONE ? TerrainId::DIRT : selectedTerrain, 0, rng);
 
 	map->players[0].canComputerPlay = true;
 	map->players[0].canHumanPlay = true;
 	map->players[1] = map->players[0];
 
-	auto cb = std::make_unique<EditorCallback>(map.get());
-
 	auto knownHeroes = LIBRARY->objtypeh->knownSubObjects(Obj::HERO);
 
-	auto addHero = [&](MapObjectSubID heroType, PlayerColor color, const int3 & position)
+	auto addHero = [&](auto & selector, PlayerColor color, const int3 & position)
 	{
-		auto it = knownHeroes.find(heroType);
-		if (it == knownHeroes.end())
-			return; // unknown hero type, nothing to add
+		selector->selectedHero->setOwner(color);
+		selector->selectedHero->pos = position;
+		for(size_t i=0; i<GameConstants::PRIMARY_SKILLS; i++)
+			selector->selectedHero->pushPrimSkill(PrimarySkill(i), std::stoi(selector->primSkillsInput[i]->getText()));
+		selector->selectedHero->clearSlots();
+		for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++)
+			if(!selector->selectedArmy->slotEmpty(SlotID(slot)))
+				selector->selectedHero->putStack(SlotID(slot), selector->selectedArmy->detachStack(SlotID(slot)));
+		map->getEditManager()->insertObject(selector->selectedHero);
+	};
 
-		auto firstHeroType = *it;
-		auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::HERO, firstHeroType);
+	addHero(heroSelector1, PlayerColor(0), int3(5, 6, 0));
+	if(selectedTown == FactionID::NONE)
+		addHero(heroSelector2, PlayerColor(1), int3(5, 5, 0));
+	else
+	{
+		auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::TOWN, 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(!heroSelector2->selectedHero)
+		{
+			for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++)
+				if(!heroSelector2->selectedArmy->slotEmpty(SlotID(slot)))
+					townObj->getArmy()->putStack(SlotID(slot), heroSelector2->selectedArmy->detachStack(SlotID(slot)));
+		}
+		else
+			addHero(heroSelector2, PlayerColor(1), int3(5, 5, 0));
 
-		auto obj = std::dynamic_pointer_cast<CGHeroInstance>(factory->create(cb.get(), templates.front()));
-		obj->setOwner(color);
-		obj->pos = position;
-		/*if(heroType == MapObjectSubID(1))
-		{*/
-			obj->pushPrimSkill(PrimarySkill::ATTACK, 100);
-			obj->getArmy()->setCreature(SlotID(0), CreatureID(1), 10000);
-		//}
-		map->getEditManager()->insertObject(obj);
-	};
-
-	addHero(MapObjectSubID(1), PlayerColor(0), int3(2, 1, 0));
-	addHero(MapObjectSubID(2), PlayerColor(1), int3(3, 1, 0));
+		map->getEditManager()->insertObject(townObj);
+	}
 
 	auto path = VCMIDirs::get().userDataPath() / "Maps";
 	boost::filesystem::create_directories(path);
@@ -122,3 +433,29 @@ void BattleOnlyModeWindow::startBattle()
 	GAME->server().setExtraOptionsInfo(extraOptions);
 	GAME->server().sendStartGame();
 }
+
+NumberInputWindow::NumberInputWindow(int initialValue, std::function<void(int)> onValueSelected)
+	: CWindowObject(BORDERED)
+{
+	OBJECT_CONSTRUCTION;
+
+	pos.w = 200;
+	pos.h = 100;
+	center();
+
+	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
+	backgroundTexture->setPlayerColor(PlayerColor(1));
+
+	buttonOk = std::make_shared<CButton>(Point(68, 60), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this, onValueSelected](){
+		int value = std::stoi(input->getText());
+		if(onValueSelected)
+			onValueSelected(value);
+		close();
+	}, EShortcut::GLOBAL_ACCEPT);
+
+	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(50, 20, 100, 20), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
+	input = std::make_shared<CTextInput>(Rect(50, 20, 100, 20), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, false);
+	input->setFilterNumber(1, 99999999);
+	input->setText(std::to_string(initialValue));
+	input->giveFocus();
+}

+ 63 - 1
client/lobby/BattleOnlyMode.h

@@ -10,23 +10,85 @@
 #pragma once
 
 #include "../windows/CWindowObject.h"
+#include "../../lib/constants/EntityIdentifiers.h"
 
 class FilledTexturePlayerColored;
 class CButton;
+class CPicture;
+class CLabel;
+class CGHeroInstance;
+class CCreatureSet;
+class EditorCallback;
+class CMap;
+class BattleOnlyModeWindow;
+class CAnimImage;
+class GraphicalPrimitiveCanvas;
+class CTextInput;
+class TransparentFilledRectangle;
 
 class BattleOnlyMode
 {
 public:
 	static void openBattleWindow();
 };
+
+class NumberInputWindow : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> backgroundTexture;
+	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
+	std::shared_ptr<CButton> buttonOk;
+	std::shared_ptr<CTextInput> input;
+public:
+	NumberInputWindow(int initialValue, std::function<void(int)> onValueSelected);
+};
+
+class BattleOnlyModeHeroSelector : public CIntObject
+{
+private:
+	BattleOnlyModeWindow& 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<CLabel>> creatureImageLabel;
+
+	void setHeroIcon();
+	void setCreatureIcons();
+public:
+	std::vector<std::shared_ptr<CAnimImage>> primSkills;
+	std::vector<std::shared_ptr<GraphicalPrimitiveCanvas>> primSkillsBorder;
+	std::vector<std::shared_ptr<CTextInput>> primSkillsInput;
+
+	std::shared_ptr<CGHeroInstance> selectedHero;
+	std::shared_ptr<CCreatureSet> selectedArmy;
+
+	BattleOnlyModeHeroSelector(BattleOnlyModeWindow& parent, Point position);
+};
+
 class BattleOnlyModeWindow : public CWindowObject
 {
+	friend class BattleOnlyModeHeroSelector;
 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<CLabel> title;
+
+	std::shared_ptr<CButton> battlegroundSelector;
+	std::shared_ptr<BattleOnlyModeHeroSelector> heroSelector1;
+	std::shared_ptr<BattleOnlyModeHeroSelector> heroSelector2;
 
+	TerrainId selectedTerrain;
+	FactionID selectedTown;
+
+	void init();
+	void setTerrainButtonText();
+	void setOkButtonEnabled();
 	void startBattle();
 public:
 	BattleOnlyModeWindow();
-};
+};

+ 23 - 1
client/render/AssetGenerator.cpp

@@ -95,10 +95,12 @@ void AssetGenerator::initialize()
 
 	for (PlayerColor color(-1); color < PlayerColor::PLAYER_LIMIT; ++color)
 	{
-		std::string name = "TowenPortalBackgroundBlue" + (color == -1 ? "" : "-" + color.toString());
+		std::string name = "TownPortalBackgroundBlue" + (color == -1 ? "" : "-" + color.toString());
 		imageFiles[ImagePath::builtin(name)] = [this, color](){ return createGateListColored(std::max(PlayerColor(0), color), PlayerColor(1)); };
 	}
 
+	imageFiles[ImagePath::builtin("heroSlotsBlue.png")] = [this](){ return createHeroSlotsColored(PlayerColor(1));};
+
 	createPaletteShiftedSprites();
 }
 
@@ -935,5 +937,25 @@ AssetGenerator::CanvasPtr AssetGenerator::createGateListColored(PlayerColor colo
 	for(auto & rect : keepOriginalRects)
 		canvas.draw(img, Point(rect.x, rect.y), rect);
 
+	std::vector<Rect> blackRect = {
+		Rect(14, 401, 66, 32),
+		Rect(227, 401, 66, 32)
+	};
+	for(auto & rect : blackRect)
+		canvas.drawBorder(rect, Colors::BLACK);
+
+	return image;
+}
+
+AssetGenerator::CanvasPtr AssetGenerator::createHeroSlotsColored(PlayerColor backColor) const
+{
+	auto img = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("OVSLOT"), EImageBlitMode::OPAQUE)->getImage(4);
+	static const std::array<ColorFilter, PlayerColor::PLAYER_LIMIT_I> filters = getColorFilters();
+	img->adjustPalette(filters[backColor.getNum()], 0);
+
+	auto image = ENGINE->renderHandler().createImage(img->dimensions(), CanvasScalingPolicy::IGNORE);
+	Canvas canvas = image->getCanvas();
+	canvas.draw(img, Point(0, 0));
+
 	return image;
 }

+ 1 - 0
client/render/AssetGenerator.h

@@ -67,6 +67,7 @@ private:
 	CanvasPtr createQuestWindow() const;
 	AnimationLayoutMap createGSPButtonClear();
 	CanvasPtr createGateListColored(PlayerColor color, PlayerColor backColor) const;
+	CanvasPtr createHeroSlotsColored(PlayerColor backColor) const;
 
 	void createPaletteShiftedSprites();
 	void generatePaletteShiftedAnimation(const AnimationPath & source, const std::vector<PaletteAnimation> & animation);

+ 2 - 0
client/render/CanvasImage.cpp

@@ -65,6 +65,8 @@ Rect CanvasImage::contentRect() const
 
 Point CanvasImage::dimensions() const
 {
+	if (scalingPolicy != CanvasScalingPolicy::IGNORE)
+		return Point(surface->w, surface->h) / ENGINE->screenHandler().getScalingFactor();
 	return {surface->w, surface->h};
 }
 

+ 15 - 4
client/widgets/Images.cpp

@@ -46,7 +46,7 @@ CPicture::CPicture(std::shared_ptr<IImage> image, const Point & position)
 	pos.w = bg->width();
 	pos.h = bg->height();
 
-	addUsedEvents(SHOW_POPUP);
+	addUsedEvents(LCLICK | SHOW_POPUP);
 }
 
 CPicture::CPicture( const ImagePath &bmpname, int x, int y )
@@ -75,7 +75,7 @@ CPicture::CPicture( const ImagePath & bmpname, const Point & position, EImageBli
 		pos.w = pos.h = 0;
 	}
 
-	addUsedEvents(SHOW_POPUP);
+	addUsedEvents(LCLICK | SHOW_POPUP);
 }
 
 CPicture::CPicture( const ImagePath & bmpname, const Point & position )
@@ -89,7 +89,7 @@ CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y)
 	pos.w = srcRect->w;
 	pos.h = srcRect->h;
 
-	addUsedEvents(SHOW_POPUP);
+	addUsedEvents(LCLICK | SHOW_POPUP);
 }
 
 CPicture::CPicture(std::shared_ptr<IImage> image, const Rect &SrcRect, int x, int y)
@@ -99,7 +99,7 @@ CPicture::CPicture(std::shared_ptr<IImage> image, const Rect &SrcRect, int x, in
 	pos.w = srcRect->w;
 	pos.h = srcRect->h;
 
-	addUsedEvents(SHOW_POPUP);
+	addUsedEvents(LCLICK | SHOW_POPUP);
 }
 
 void CPicture::show(Canvas & to)
@@ -137,11 +137,22 @@ void CPicture::setPlayerColor(PlayerColor player)
 	bg->playerColored(player);
 }
 
+void CPicture::addLClickCallback(const std::function<void()> & callback)
+{
+	lCallback = callback;
+}
+
 void CPicture::addRClickCallback(const std::function<void()> & callback)
 {
 	rCallback = callback;
 }
 
+void CPicture::clickPressed(const Point & cursorPosition)
+{
+	if(lCallback)
+		lCallback();
+}
+
 void CPicture::showPopupWindow(const Point & cursorPosition)
 {
 	if(rCallback)

+ 3 - 0
client/widgets/Images.h

@@ -27,6 +27,7 @@ enum class EImageBlitMode : uint8_t;
 class CPicture : public CIntObject
 {
 	std::shared_ptr<IImage> bg;
+	std::function<void()> lCallback;
 	std::function<void()> rCallback;
 
 public:
@@ -60,10 +61,12 @@ public:
 	void scaleTo(Point size);
 	void setPlayerColor(PlayerColor player);
 
+	void addLClickCallback(const std::function<void()> & callback);
 	void addRClickCallback(const std::function<void()> & callback);
 
 	void show(Canvas & to) override;
 	void showAll(Canvas & to) override;
+	void clickPressed(const Point & cursorPosition) override;
 	void showPopupWindow(const Point & cursorPosition) override;
 };
 

+ 5 - 3
client/widgets/MiscWidgets.cpp

@@ -623,13 +623,15 @@ void MoraleLuckBox::set(const AFactionMember * node)
 	else if(morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_MORALE))
 	{
 		auto noMorale = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_MORALE));
-		text += "\n" + noMorale->Description(GAME->interface()->cb.get());
+		if(GAME->interface())
+			text += "\n" + noMorale->Description(GAME->interface()->cb.get());
 		component.value = 0;
 	}
 	else if (!morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK))
 	{
 		auto noLuck = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_LUCK));
-		text += "\n" + noLuck->Description(GAME->interface()->cb.get());
+		if(GAME->interface())
+			text += "\n" + noLuck->Description(GAME->interface()->cb.get());
 		component.value = 0;
 	}
 	else
@@ -637,7 +639,7 @@ void MoraleLuckBox::set(const AFactionMember * node)
 		std::string addInfo = "";
 		for(auto & bonus : * modifierList)
 		{
-			if(bonus->val) {
+			if(GAME->interface() && bonus->val) {
 				const std::string& description = bonus->Description(GAME->interface()->cb.get());
 				//arraytxt already contains \n
 				if (description.size() && description[0] != '\n')

+ 2 - 2
client/windows/CCreatureWindow.cpp

@@ -952,8 +952,8 @@ void CStackWindow::initSections()
 {
 	OBJECT_CONSTRUCTION;
 
-	bool showArt = GAME->interface()->cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_ARTIFACT) && info->commander == nullptr && info->stackNode;
-	bool showExp = (GAME->interface()->cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE) || info->commander != nullptr) && info->stackNode;
+	bool showArt = GAME->interface() && GAME->interface()->cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_ARTIFACT) && info->commander == nullptr && info->stackNode;
+	bool showExp = ((GAME->interface() && GAME->interface()->cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) || info->commander != nullptr) && info->stackNode;
 
 	mainSection = std::make_shared<MainSection>(this, pos.h, showExp, showArt);
 

+ 3 - 1
client/windows/CWindowObject.cpp

@@ -86,7 +86,9 @@ std::shared_ptr<CPicture> CWindowObject::createBg(const ImagePath & imageName, b
 		return nullptr;
 
 	auto image = std::make_shared<CPicture>(imageName, Point(0,0), EImageBlitMode::OPAQUE);
-	if(playerColored)
+	if(!GAME->interface())
+		image->setPlayerColor(PlayerColor(1)); // in main menu we use blue
+	else if(playerColored)
 		image->setPlayerColor(GAME->interface()->playerID);
 	return image;
 }

+ 15 - 14
client/windows/GUIClasses.cpp

@@ -1535,8 +1535,9 @@ CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::st
 	index(_id)
 {
 	OBJECT_CONSTRUCTION;
-	if(parent->images.size() > index)
-		icon = std::make_shared<CPicture>(parent->images[index], Point(1, 1));
+	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));
 	border = std::make_shared<CPicture>(ImagePath::builtin("TPGATES"));
 	pos = border->pos;
 
@@ -1581,8 +1582,8 @@ void CObjectListWindow::CItem::showPopupWindow(const Point & cursorPosition)
 		parent->onPopup(index);
 }
 
-CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection, std::vector<std::shared_ptr<IImage>> images, bool searchBoxEnabled)
-	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")),
+CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection, std::vector<std::shared_ptr<IImage>> images, bool searchBoxEnabled, bool blue)
+	: CWindowObject(PLAYER_COLORED, ImagePath::builtin(blue ? "TownPortalBackgroundBlue" : "TPGATE")),
 	onSelect(Callback),
 	selected(initialSelection),
 	images(images)
@@ -1601,12 +1602,12 @@ CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::share
 	}
 	itemsVisible = items;
 
-	init(titleWidget_, _title, _descr, searchBoxEnabled);
+	init(titleWidget_, _title, _descr, searchBoxEnabled, blue);
 	list->scrollTo(std::min(static_cast<int>(initialSelection + 4), static_cast<int>(items.size() - 1))); // 4 is for centering (list have 9 elements)
 }
 
-CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection, std::vector<std::shared_ptr<IImage>> images, bool searchBoxEnabled)
-	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")),
+CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection, std::vector<std::shared_ptr<IImage>> images, bool searchBoxEnabled, bool blue)
+	: CWindowObject(PLAYER_COLORED, ImagePath::builtin(blue ? "TownPortalBackgroundBlue" : "TPGATE")),
 	onSelect(Callback),
 	selected(initialSelection),
 	images(images)
@@ -1625,17 +1626,17 @@ CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, st
 	}
 	itemsVisible = items;
 
-	init(titleWidget_, _title, _descr, searchBoxEnabled);
+	init(titleWidget_, _title, _descr, searchBoxEnabled, blue);
 	list->scrollTo(std::min(static_cast<int>(initialSelection + 4), static_cast<int>(items.size() - 1))); // 4 is for centering (list have 9 elements)
 }
 
-void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled)
+void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled, bool blue)
 {
 	titleWidget = titleWidget_;
 
 	title = std::make_shared<CLabel>(152, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, _title);
 	descr = std::make_shared<CLabel>(145, 133, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _descr);
-	exit = std::make_shared<CButton>( Point(228, 402), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL);
+	exit = std::make_shared<CButton>( Point(228, 402), AnimationPath::builtin(blue ? "MuBcanc" : "ICANCEL.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL);
 
 	if(titleWidget)
 	{
@@ -1644,10 +1645,10 @@ void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::stri
 		titleWidget->pos.y = 75 + pos.y - titleWidget->pos.h/2;
 	}
 	list = std::make_shared<CListBox>(std::bind(&CObjectListWindow::genItem, this, _1),
-		Point(14, 151), Point(0, 25), 9, itemsVisible.size(), 0, 1, Rect(262, -32, 256, 256) );
+		Point(14, 151), Point(0, 25), 9, itemsVisible.size(), 0, 1 + (blue ? 4 : 0), Rect(262, -32, 256, 256) );
 	list->setRedrawParent(true);
 
-	ok = std::make_shared<CButton>(Point(15, 402), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT);
+	ok = std::make_shared<CButton>(Point(15, 402), AnimationPath::builtin(blue ? "MuBchck" : "IOKAY.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT);
 	ok->block(!list->size());
 
 	if(!searchBoxEnabled)
@@ -1655,8 +1656,8 @@ void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::stri
 
 	Rect r(50, 90, pos.w - 100, 16);
 	const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75);
-	const ColorRGBA borderColor = ColorRGBA(128, 100, 75);
-	const ColorRGBA grayedColor = ColorRGBA(158, 130, 105);
+	const ColorRGBA borderColor = blue ? ColorRGBA(75, 84, 128) : ColorRGBA(128, 100, 75);
+	const ColorRGBA grayedColor = blue ? ColorRGBA(105, 127, 159) : ColorRGBA(158, 130, 105);
 	searchBoxRectangle = std::make_shared<TransparentFilledRectangle>(r.resize(1), rectangleColor, borderColor);
 	searchBoxDescription = std::make_shared<CLabel>(r.center().x, r.center().y, FONT_SMALL, ETextAlignment::CENTER, grayedColor, LIBRARY->generaltexth->translate("vcmi.spellBook.search"));
 

+ 3 - 3
client/windows/GUIClasses.h

@@ -205,7 +205,7 @@ class CObjectListWindow : public CWindowObject
 	std::vector< std::pair<int, std::string> > items; //all items present in list
 	std::vector< std::pair<int, std::string> > itemsVisible; //visible items present in list
 
-	void init(std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled);
+	void init(std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled, bool blue);
 	void trimTextIfTooWide(std::string & text, int id) const; // trim item's text to fit within window's width
 	void itemsSearchCallback(const std::string & text);
 	void exitPressed();
@@ -219,8 +219,8 @@ public:
 	/// Callback will be called when OK button is pressed, returns id of selected item. initState = initially selected item
 	/// Image can be nullptr
 	///item names will be taken from map objects
-	CObjectListWindow(const std::vector<int> &_items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection = 0, std::vector<std::shared_ptr<IImage>> images = {}, bool searchBoxEnabled = false);
-	CObjectListWindow(const std::vector<std::string> &_items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection = 0, std::vector<std::shared_ptr<IImage>> images = {}, bool searchBoxEnabled = false);
+	CObjectListWindow(const std::vector<int> &_items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection = 0, std::vector<std::shared_ptr<IImage>> images = {}, bool searchBoxEnabled = false, bool blue = false);
+	CObjectListWindow(const std::vector<std::string> &_items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection = 0, std::vector<std::shared_ptr<IImage>> images = {}, bool searchBoxEnabled = false, bool blue = false);
 
 	std::shared_ptr<CIntObject> genItem(size_t index);
 	void elementSelected();//call callback and close this window

+ 7 - 3
server/processors/TurnOrderProcessor.cpp

@@ -20,6 +20,7 @@
 #include "../../lib/mapping/CMap.h"
 #include "../../lib/mapObjects/CGObjectInstance.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/pathfinder/CPathfinder.h"
 #include "../../lib/pathfinder/PathfinderOptions.h"
@@ -367,10 +368,13 @@ void TurnOrderProcessor::onGameStarted()
 {
 	if(gameHandler->gameInfo().getMapHeader()->battleOnly)
 	{
+		auto towns = gameHandler->gameState().getMap().getObjects<CGTownInstance>();
 		auto heroes = gameHandler->gameState().getMap().getObjects<CGHeroInstance>();
-		auto hero1 = heroes.at(0);
-		auto hero2 = heroes.at(1);
-		gameHandler->startBattle(hero1, hero2);
+		if(!towns.size() && heroes.size() == 2)
+			gameHandler->startBattle(heroes.at(0), heroes.at(1));
+		else
+			towns.at(0)->onHeroVisit(*gameHandler, heroes.at(0));
+
 		return;
 	}