Kaynağa Gözat

Merge pull request #2041 from IvanSavenko/bugfixing_for_hotfix

Fixes for 1.2.1
Ivan Savenko 2 yıl önce
ebeveyn
işleme
9296306fa3
42 değiştirilmiş dosya ile 217 ekleme ve 114 silme
  1. 0 40
      AI/Nullkiller/AIGateway.cpp
  2. 0 5
      AI/Nullkiller/AIGateway.h
  3. 47 0
      AI/Nullkiller/Analyzers/HeroManager.cpp
  4. 7 1
      AI/Nullkiller/Analyzers/HeroManager.h
  5. 1 1
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  6. 7 7
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  7. 1 1
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  8. 1 1
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  9. 1 1
      AI/Nullkiller/Engine/Nullkiller.cpp
  10. 1 0
      AI/Nullkiller/Engine/Nullkiller.h
  11. 0 2
      AI/Nullkiller/Goals/RecruitHero.cpp
  12. 1 1
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  13. 2 4
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  14. 1 0
      CMakeLists.txt
  15. 20 0
      ChangeLog.md
  16. BIN
      Mods/vcmi/Data/debug/cached.png
  17. BIN
      Mods/vcmi/Data/debug/spellRange.png
  18. 1 0
      client/Client.cpp
  19. 14 2
      client/adventureMap/CAdvMapInt.cpp
  20. 1 1
      client/battle/BattleAnimationClasses.cpp
  21. 1 1
      client/lobby/CLobbyScreen.cpp
  22. 3 4
      client/lobby/CSavingScreen.cpp
  23. 0 1
      client/lobby/CSavingScreen.h
  24. 4 0
      client/lobby/CScenarioInfoScreen.cpp
  25. 25 1
      client/lobby/RandomMapTab.cpp
  26. 4 0
      client/mapView/IMapRendererContext.h
  27. 12 5
      client/mapView/MapRenderer.cpp
  28. 3 12
      client/mapView/MapRenderer.h
  29. 19 0
      client/mapView/MapRendererContext.cpp
  30. 4 0
      client/mapView/MapRendererContext.h
  31. 2 1
      client/mapView/MapView.cpp
  32. 2 1
      client/mapView/MapViewController.cpp
  33. 4 4
      client/windows/CSpellWindow.cpp
  34. 1 1
      cmake_modules/VersionDefinition.cmake
  35. 7 1
      debian/changelog
  36. 1 0
      launcher/eu.vcmi.VCMI.metainfo.xml
  37. 1 1
      launcher/firstLaunch/firstlaunch_moc.cpp
  38. 5 5
      launcher/jsonutils.cpp
  39. 1 1
      lib/CStack.cpp
  40. 3 3
      lib/mapObjects/CGMarket.cpp
  41. 3 0
      mapeditor/inspector/inspector.cpp
  42. 6 5
      server/CGameHandler.cpp

+ 0 - 40
AI/Nullkiller/AIGateway.cpp

@@ -1059,27 +1059,6 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
 	}
 }
 
-bool AIGateway::canRecruitAnyHero(const CGTownInstance * t) const
-{
-	//TODO: make gathering gold, building tavern or conquering town (?) possible subgoals
-	if(!t)
-		t = findTownWithTavern();
-
-	if(!t || !townHasFreeTavern(t))
-		return false;
-
-	if(cb->getResourceAmount(Res::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
-		return false;
-	if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
-		return false;
-	if(cb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
-		return false;
-	if(!cb->getAvailableHeroes(t).size())
-		return false;
-
-	return true;
-}
-
 void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side)
 {
 	NET_EVENT_HANDLER;
@@ -1155,16 +1134,6 @@ void AIGateway::addVisitableObj(const CGObjectInstance * obj)
 	}
 }
 
-HeroPtr AIGateway::getHeroWithGrail() const
-{
-	for(const CGHeroInstance * h : cb->getHeroesInfo())
-	{
-		if(h->hasArt(ArtifactID::GRAIL))
-			return h;
-	}
-	return nullptr;
-}
-
 bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 {
 	if(h->inTownGarrison && h->visitedTown)
@@ -1432,15 +1401,6 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
 	}
 }
 
-const CGTownInstance * AIGateway::findTownWithTavern() const
-{
-	for(const CGTownInstance * t : cb->getTownsInfo())
-		if(townHasFreeTavern(t))
-			return t;
-
-	return nullptr;
-}
-
 void AIGateway::endTurn()
 {
 	logAi->info("Player %d (%s) ends turn", playerID, playerID.getStr());

+ 0 - 5
AI/Nullkiller/AIGateway.h

@@ -198,11 +198,6 @@ public:
 	void retrieveVisitableObjs();
 	virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;
 
-	HeroPtr getHeroWithGrail() const;
-
-	const CGTownInstance * findTownWithTavern() const;
-	bool canRecruitAnyHero(const CGTownInstance * t = NULL) const;
-
 	void requestSent(const CPackForServer * pack, int requestID) override;
 	void answerQuery(QueryID queryID, int selection);
 	//special function that can be called ONLY from game events handling thread and will send request ASAP

+ 47 - 0
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -12,6 +12,8 @@
 #include "../Engine/Nullkiller.h"
 #include "../../../lib/mapObjects/MapObjects.h"
 #include "../../../lib/CHeroHandler.h"
+#include "../../../lib/GameSettings.h"
+#include "../../../lib/CGameState.h"
 
 namespace NKAI
 {
@@ -179,6 +181,51 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
 	return evaluateFightingStrength(hero);
 }
 
+bool HeroManager::canRecruitHero(const CGTownInstance * town) const
+{
+	if(!town)
+		town = findTownWithTavern();
+
+	if(!town || !townHasFreeTavern(town))
+		return false;
+
+	if(cb->getResourceAmount(Res::GOLD) < GameConstants::HERO_GOLD_COST)
+		return false;
+
+	const bool includeGarnisoned = true;
+	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
+
+	if(heroCount >= ALLOWED_ROAMING_HEROES)
+		return false;
+
+	if(heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
+		return false;
+
+	if(!cb->getAvailableHeroes(town).size())
+		return false;
+
+	return true;
+}
+
+const CGTownInstance * HeroManager::findTownWithTavern() const
+{
+	for(const CGTownInstance * t : cb->getTownsInfo())
+		if(townHasFreeTavern(t))
+			return t;
+
+	return nullptr;
+}
+
+const CGHeroInstance * HeroManager::findHeroWithGrail() const
+{
+	for(const CGHeroInstance * h : cb->getHeroesInfo())
+	{
+		if(h->hasArt(ArtifactID::GRAIL))
+			return h;
+	}
+	return nullptr;
+}
+
 SecondarySkillScoreMap::SecondarySkillScoreMap(std::map<SecondarySkill, float> scoreMap)
 	:scoreMap(scoreMap)
 {

+ 7 - 1
AI/Nullkiller/Analyzers/HeroManager.h

@@ -30,6 +30,8 @@ public:
 	virtual void update() = 0;
 	virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
 	virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
+	virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
+	virtual const CGHeroInstance * findHeroWithGrail() const = 0;
 };
 
 class DLL_EXPORT ISecondarySkillRule
@@ -57,20 +59,24 @@ private:
 	static SecondarySkillEvaluator scountSkillsScores;
 
 	CCallback * cb; //this is enough, but we downcast from CCallback
+	const Nullkiller * ai;
 	std::map<HeroPtr, HeroRole> heroRoles;
 
 public:
-	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB) {}
+	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB), ai(ai) {}
 	const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
 	HeroRole getHeroRole(const HeroPtr & hero) const override;
 	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
 	void update() override;
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
 	float evaluateHero(const CGHeroInstance * hero) const override;
+	bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
+	const CGHeroInstance * findHeroWithGrail() const override;
 
 private:
 	float evaluateFightingStrength(const CGHeroInstance * hero) const;
 	float evaluateSpeciality(const CGHeroInstance * hero) const;
+	const CGTownInstance * findTownWithTavern() const;
 };
 
 // basic skill scores. missing skills will have score of 0

+ 1 - 1
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -209,7 +209,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 	{
 		captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
 
-		if(tasks.empty())
+		if(tasks.empty() || ai->nullkiller->getScanDepth() == ScanDepth::FULL)
 			captureObjects(ai->nullkiller->objectClusterizer->getFarObjects());
 	}
 

+ 7 - 7
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -58,13 +58,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
 	auto treats = { treatNode.maximumDanger, treatNode.fastestDanger };
 
-	if(!treatNode.fastestDanger.hero)
-	{
-		logAi->trace("No treat found for town %s", town->getNameTranslated());
-
-		return;
-	}
-
 	int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
 
 	if(town->garrisonHero)
@@ -91,6 +84,13 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 		return;
 	}
+
+	if(!treatNode.fastestDanger.hero)
+	{
+		logAi->trace("No treat found for town %s", town->getNameTranslated());
+
+		return;
+	}
 	
 	uint64_t reinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
 

+ 1 - 1
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -53,7 +53,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
 
 	for(auto town : towns)
 	{
-		if(ai->canRecruitAnyHero(town))
+		if(ai->nullkiller->heroManager->canRecruitHero(town))
 		{
 			auto availableHeroes = cb->getAvailableHeroes(town);
 

+ 1 - 1
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -66,7 +66,7 @@ const CGHeroInstance * getNearestHero(const CGTownInstance * town)
 
 bool needToRecruitHero(const CGTownInstance * startupTown)
 {
-	if(!ai->canRecruitAnyHero(startupTown))
+	if(!ai->nullkiller->heroManager->canRecruitHero(startupTown))
 		return false;
 
 	if(!startupTown->garrisonHero && !startupTown->visitingHero)

+ 1 - 1
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -117,7 +117,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi
 void Nullkiller::resetAiState()
 {
 	lockedResources = TResources();
-	scanDepth = ScanDepth::SMALL;
+	scanDepth = ScanDepth::FULL;
 	playerID = ai->playerID;
 	lockedHeroes.clear();
 	dangerHitMap->reset();

+ 1 - 0
AI/Nullkiller/Engine/Nullkiller.h

@@ -88,6 +88,7 @@ public:
 	int32_t getFreeGold() const { return getFreeResources()[Res::GOLD]; }
 	void lockResources(const TResources & res);
 	const TResources & getLockedResources() const { return lockedResources; }
+	ScanDepth getScanDepth() const { return scanDepth; }
 
 private:
 	void resetAiState();

+ 0 - 2
AI/Nullkiller/Goals/RecruitHero.cpp

@@ -33,8 +33,6 @@ void RecruitHero::accept(AIGateway * ai)
 {
 	auto t = town;
 
-	if(!t) t = ai->findTownWithTavern();
-
 	if(!t)
 	{
 		throw cannotFulfillGoalException("No town to recruit hero!");

+ 1 - 1
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -1093,7 +1093,7 @@ void AINodeStorage::calculateTownPortal(
 			if(nodeOptional)
 			{
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 1
-				logAi->trace("Adding town portal node at %s", targetTown->name);
+				logAi->trace("Adding town portal node at %s", targetTown->getObjectName());
 #endif
 				output.push_back(nodeOptional.get());
 			}

+ 2 - 4
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -53,15 +53,13 @@ namespace AIPathfinding
 
 		for(const CGTownInstance * t : cb->getTownsInfo())
 		{
-			// do not allow ally shipyards because of bug
-			if(t->hasBuilt(BuildingID::SHIPYARD) && t->getOwner() == ai->playerID)
+			if(t->hasBuilt(BuildingID::SHIPYARD))
 				shipyards.push_back(t);
 		}
 
 		for(const CGObjectInstance * obj : ai->memory->visitableObjs)
 		{
-			// do not allow ally shipyards because of bug
-			if(obj->ID != Obj::TOWN && obj->getOwner() == ai->playerID) //towns were handled in the previous loop
+			if(obj->ID != Obj::TOWN) //towns were handled in the previous loop
 			{
 				if(const IShipyard * shipyard = IShipyard::castFrom(obj))
 					shipyards.push_back(shipyard);

+ 1 - 0
CMakeLists.txt

@@ -246,6 +246,7 @@ if(MINGW OR MSVC)
 		add_definitions(-D_CRT_SECURE_NO_WARNINGS)
 		add_definitions(-D_SCL_SECURE_NO_WARNINGS)
 
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /utf-8")
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj")
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4250") # 4250: 'class1' : inherits 'class2::member' via dominance
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4251") # 4251: class 'xxx' needs to have dll-interface to be used by clients of class 'yyy'

+ 20 - 0
ChangeLog.md

@@ -1,3 +1,23 @@
+# 1.2.0 -> 1.2.1
+
+### GENERAL:
+* Implemented spell range overlay for Dimension Door and Scuttle Boat
+* Fixed movement cost penalty from terrain
+* Fixed empty Black Market on game start
+* Fixed bad morale happening after waiting
+* Fixed good morale happening after defeating last enemy unit
+* Fixed death animation of Efreeti killed by petrification attack
+* Fixed crash on leaving to main menu from battle in hotseat mode
+* Adventure map spells are no longer visible on units in battle
+* Attempt to cast spell with no valid targets in hotseat will show appropriate error message
+* RMG settings will now show all existing in game templates and not just those suitable for current settings
+* RMG settings (map size and two-level maps) that are not compatible with current template will be blocked
+* Fixed centering of scenario information window
+* Fixed crash on empty save game list after filtering
+* Fixed blocked progress in Launcher on language detection failure
+* Launcher will now correctly handle selection of Ddata directory in H3 install
+* Map editor will now correctly save message property for events and pandoras
+
 # 1.1.1 -> 1.2.0
 
 ### GENERAL:

BIN
Mods/vcmi/Data/debug/cached.png


BIN
Mods/vcmi/Data/debug/spellRange.png


+ 1 - 0
client/Client.cpp

@@ -374,6 +374,7 @@ void CClient::endGame()
 	//threads cleanup has to be after gs cleanup and before battleints cleanup to stop tacticThread
 	cleanThreads();
 
+	CPlayerInterface::battleInt.reset();
 	playerint.clear();
 	battleints.clear();
 	battleCallbacks.clear();

+ 14 - 2
client/adventureMap/CAdvMapInt.cpp

@@ -1076,8 +1076,11 @@ void CAdvMapInt::onTileLeftClicked(const int3 &mapPos)
 	const CGObjectInstance *topBlocking = getActiveObject(mapPos);
 
 	int3 selPos = selection->getSightCenter();
-	if(spellBeingCasted && isInScreenRange(selPos, mapPos))
+	if(spellBeingCasted)
 	{
+		if (!isInScreenRange(selPos, mapPos))
+			return;
+
 		const TerrainTile *heroTile = LOCPLINT->cb->getTile(selPos);
 
 		switch(spellBeingCasted->id)
@@ -1179,11 +1182,15 @@ void CAdvMapInt::onTileHovered(const int3 &mapPos)
 		switch(spellBeingCasted->id)
 		{
 		case SpellID::SCUTTLE_BOAT:
-			if(objAtTile && objAtTile->ID == Obj::BOAT)
+			{
+			int3 hpos = selection->getSightCenter();
+
+			if(objAtTile && objAtTile->ID == Obj::BOAT && isInScreenRange(hpos, mapPos))
 				CCS->curh->set(Cursor::Map::SCUTTLE_BOAT);
 			else
 				CCS->curh->set(Cursor::Map::POINTER);
 			return;
+			}
 		case SpellID::DIMENSION_DOOR:
 			{
 				const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false);
@@ -1342,6 +1349,8 @@ void CAdvMapInt::enterCastingMode(const CSpell * sp)
 {
 	assert(sp->id == SpellID::SCUTTLE_BOAT  ||  sp->id == SpellID::DIMENSION_DOOR);
 	spellBeingCasted = sp;
+	Settings config = settings.write["session"]["showSpellRange"];
+	config->Bool() = true;
 
 	deactivate();
 	terrain->activate();
@@ -1356,6 +1365,9 @@ void CAdvMapInt::leaveCastingMode(bool cast, int3 dest)
 	terrain->deactivate();
 	activate();
 
+	Settings config = settings.write["session"]["showSpellRange"];
+	config->Bool() = false;
+
 	if(cast)
 		LOCPLINT->cb->castSpell(curHero(), id, dest);
 	else

+ 1 - 1
client/battle/BattleAnimationClasses.cpp

@@ -137,7 +137,7 @@ bool StackActionAnimation::init()
 
 StackActionAnimation::~StackActionAnimation()
 {
-	if (stack->isFrozen())
+	if (stack->isFrozen() && currGroup != ECreatureAnimType::DEATH && currGroup != ECreatureAnimType::DEATH_RANGED)
 		myAnim->setType(ECreatureAnimType::HOLDING);
 	else
 		myAnim->setType(nextGroup);

+ 1 - 1
client/lobby/CLobbyScreen.cpp

@@ -197,7 +197,7 @@ void CLobbyScreen::updateAfterStateChange()
 		}
 	}
 	
-	if(curTab == tabRand && CSH->si->mapGenOptions)
+	if(curTab && curTab == tabRand && CSH->si->mapGenOptions)
 		tabRand->setMapGenOptions(CSH->si->mapGenOptions);
 }
 

+ 3 - 4
client/lobby/CSavingScreen.cpp

@@ -31,8 +31,6 @@ CSavingScreen::CSavingScreen()
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	center(pos);
-	// TODO: we should really use std::shared_ptr for passing StartInfo around.
-	localSi = new StartInfo(*LOCPLINT->cb->getStartInfo());
 	localMi = std::make_shared<CMapInfo>();
 	localMi->mapHeader = std::unique_ptr<CMapHeader>(new CMapHeader(*LOCPLINT->cb->getMapHeader()));
 
@@ -52,7 +50,9 @@ const CMapInfo * CSavingScreen::getMapInfo()
 
 const StartInfo * CSavingScreen::getStartInfo()
 {
-	return localSi;
+	if (localMi)
+		return localMi->scenarioOptionsOfSave;
+	return LOCPLINT->cb->getStartInfo();
 }
 
 void CSavingScreen::changeSelection(std::shared_ptr<CMapInfo> to)
@@ -61,7 +61,6 @@ void CSavingScreen::changeSelection(std::shared_ptr<CMapInfo> to)
 		return;
 
 	localMi = to;
-	localSi = localMi->scenarioOptionsOfSave;
 	card->changeSelection();
 }
 

+ 0 - 1
client/lobby/CSavingScreen.h

@@ -23,7 +23,6 @@ class CSelectionBase;
 class CSavingScreen : public CSelectionBase
 {
 public:
-	const StartInfo * localSi;
 	std::shared_ptr<CMapInfo> localMi;
 
 	CSavingScreen();

+ 4 - 0
client/lobby/CScenarioInfoScreen.cpp

@@ -26,6 +26,10 @@
 CScenarioInfoScreen::CScenarioInfoScreen()
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos.w = 800;
+	pos.h = 600;
+	pos = center();
+
 	localSi = new StartInfo(*LOCPLINT->cb->getStartInfo());
 	localMi = new CMapInfo();
 	localMi->mapHeader = std::unique_ptr<CMapHeader>(new CMapHeader(*LOCPLINT->cb->getMapHeader()));

+ 25 - 1
client/lobby/RandomMapTab.cpp

@@ -242,9 +242,29 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
 	}
 	
 	if(auto w = widget<CToggleGroup>("groupMapSize"))
+	{
+		for(auto toggle : w->buttons)
+		{
+			if(auto button = std::dynamic_pointer_cast<CToggleButton>(toggle.second))
+			{
+				const auto & mapSizes = getPossibleMapSizes();
+				int3 size( mapSizes[toggle.first], mapSizes[toggle.first], 1 + mapGenOptions->getHasTwoLevels());
+
+				bool sizeAllowed = !mapGenOptions->getMapTemplate() || mapGenOptions->getMapTemplate()->matchesSize(size);
+				button->block(!sizeAllowed);
+			}
+		}
 		w->setSelected(vstd::find_pos(getPossibleMapSizes(), opts->getWidth()));
+	}
 	if(auto w = widget<CToggleButton>("buttonTwoLevels"))
+	{
+		int3 size( opts->getWidth(), opts->getWidth(), 2);
+
+		bool undergoundAllowed = !mapGenOptions->getMapTemplate() || mapGenOptions->getMapTemplate()->matchesSize(size);
+
 		w->setSelected(opts->getHasTwoLevels());
+		w->block(!undergoundAllowed);
+	}
 	if(auto w = widget<CToggleGroup>("groupMaxPlayers"))
 	{
 		w->setSelected(opts->getPlayerCount());
@@ -408,7 +428,11 @@ TemplatesDropBox::TemplatesDropBox(RandomMapTab & randomMapTab, int3 size):
 	REGISTER_BUILDER("templateListItem", &TemplatesDropBox::buildListItem);
 	
 	curItems = VLC->tplh->getTemplates();
-	vstd::erase_if(curItems, [size](const CRmgTemplate * t){return !t->matchesSize(size);});
+
+	boost::range::sort(curItems, [](const CRmgTemplate * a, const CRmgTemplate * b){
+		return a->getName() < b->getName();
+	});
+
 	curItems.insert(curItems.begin(), nullptr); //default template
 	
 	const JsonNode config(ResourceID("config/widgets/randomMapTemplateWidget.json"));

+ 4 - 0
client/mapView/IMapRendererContext.h

@@ -86,4 +86,8 @@ public:
 	virtual bool showGrid() const = 0;
 	virtual bool showVisitable() const = 0;
 	virtual bool showBlocked() const = 0;
+
+	/// if true, spell range for teleport / scuttle boat will be visible
+	virtual bool showSpellRange(const int3 & position) const = 0;
+
 };

+ 12 - 5
client/mapView/MapRenderer.cpp

@@ -584,15 +584,16 @@ uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 &
 	return 0xff-1;
 }
 
-MapRendererDebug::MapRendererDebug()
+MapRendererOverlay::MapRendererOverlay()
 	: imageGrid(IImage::createFromFile("debug/grid", EImageBlitMode::ALPHA))
 	, imageBlocked(IImage::createFromFile("debug/blocked", EImageBlitMode::ALPHA))
 	, imageVisitable(IImage::createFromFile("debug/visitable", EImageBlitMode::ALPHA))
+	, imageSpellRange(IImage::createFromFile("debug/spellRange", EImageBlitMode::ALPHA))
 {
 
 }
 
-void MapRendererDebug::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates)
+void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates)
 {
 	if(context.showGrid())
 		target.draw(imageGrid, Point(0,0));
@@ -618,9 +619,12 @@ void MapRendererDebug::renderTile(IMapRendererContext & context, Canvas & target
 		if (context.showVisitable() && visitable)
 			target.draw(imageVisitable, Point(0,0));
 	}
+
+	if (context.showSpellRange(coordinates))
+		target.draw(imageSpellRange, Point(0,0));
 }
 
-uint8_t MapRendererDebug::checksum(IMapRendererContext & context, const int3 & coordinates)
+uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & coordinates)
 {
 	uint8_t result = 0;
 
@@ -633,6 +637,9 @@ uint8_t MapRendererDebug::checksum(IMapRendererContext & context, const int3 & c
 	if (context.showGrid())
 		result += 4;
 
+	if (context.showSpellRange(coordinates))
+		result += 8;
+
 	return result;
 }
 
@@ -766,7 +773,7 @@ MapRenderer::TileChecksum MapRenderer::getTileChecksum(IMapRendererContext & con
 			result[3] = rendererRoad.checksum(context, coordinates);
 		result[4] = rendererObjects.checksum(context, coordinates);
 		result[5] = rendererPath.checksum(context, coordinates);
-		result[6] = rendererDebug.checksum(context, coordinates);
+		result[6] = rendererOverlay.checksum(context, coordinates);
 
 		if(!context.isVisible(coordinates))
 			result[7] = rendererFow.checksum(context, coordinates);
@@ -800,7 +807,7 @@ void MapRenderer::renderTile(IMapRendererContext & context, Canvas & target, con
 
 		rendererObjects.renderTile(context, target, coordinates);
 		rendererPath.renderTile(context, target, coordinates);
-		rendererDebug.renderTile(context, target, coordinates);
+		rendererOverlay.renderTile(context, target, coordinates);
 
 		if(!context.isVisible(coordinates))
 			rendererFow.renderTile(context, target, coordinates);

+ 3 - 12
client/mapView/MapRenderer.h

@@ -129,21 +129,12 @@ public:
 	void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates);
 };
 
-class MapRendererDebug
+class MapRendererOverlay
 {
 	std::shared_ptr<IImage> imageGrid;
 	std::shared_ptr<IImage> imageVisitable;
 	std::shared_ptr<IImage> imageBlocked;
-public:
-	MapRendererDebug();
-
-	uint8_t checksum(IMapRendererContext & context, const int3 & coordinates);
-	void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates);
-};
-
-class MapRendererOverlay
-{
-	std::unique_ptr<CAnimation> iconsStorage;
+	std::shared_ptr<IImage> imageSpellRange;
 public:
 	MapRendererOverlay();
 
@@ -160,7 +151,7 @@ class MapRenderer
 	MapRendererFow rendererFow;
 	MapRendererObjects rendererObjects;
 	MapRendererPath rendererPath;
-	MapRendererDebug rendererDebug;
+	MapRendererOverlay rendererOverlay;
 
 public:
 	using TileChecksum = std::array<uint8_t, 8>;

+ 19 - 0
client/mapView/MapRendererContext.cpp

@@ -22,6 +22,7 @@
 #include "../../lib/CPathfinder.h"
 #include "../../lib/Point.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/mapping/CMap.h"
 
 MapRendererBaseContext::MapRendererBaseContext(const MapRendererContextState & viewState)
@@ -199,6 +200,11 @@ bool MapRendererBaseContext::showBlocked() const
 	return false;
 }
 
+bool MapRendererBaseContext::showSpellRange(const int3 & position) const
+{
+	return false;
+}
+
 MapRendererAdventureContext::MapRendererAdventureContext(const MapRendererContextState & viewState)
 	: MapRendererBaseContext(viewState)
 {
@@ -266,6 +272,19 @@ bool MapRendererAdventureContext::showBlocked() const
 	return settingShowBlocked;
 }
 
+bool MapRendererAdventureContext::showSpellRange(const int3 & position) const
+{
+	if (!settingSpellRange)
+		return false;
+
+	auto hero = adventureInt->curHero();
+
+	if (!hero)
+		return false;
+
+	return !isInScreenRange(hero->getSightCenter(), position);
+}
+
 MapRendererAdventureTransitionContext::MapRendererAdventureTransitionContext(const MapRendererContextState & viewState)
 	: MapRendererAdventureContext(viewState)
 {

+ 4 - 0
client/mapView/MapRendererContext.h

@@ -58,6 +58,7 @@ public:
 	bool showGrid() const override;
 	bool showVisitable() const override;
 	bool showBlocked() const override;
+	bool showSpellRange(const int3 & position) const override;
 };
 
 class MapRendererAdventureContext : public MapRendererBaseContext
@@ -67,6 +68,7 @@ public:
 	bool settingShowGrid = false;
 	bool settingShowVisitable = false;
 	bool settingShowBlocked = false;
+	bool settingSpellRange= false;
 	bool settingsAdventureObjectAnimation = true;
 	bool settingsAdventureTerrainAnimation = true;
 
@@ -80,6 +82,8 @@ public:
 	bool showGrid() const override;
 	bool showVisitable() const override;
 	bool showBlocked() const override;
+
+	bool showSpellRange(const int3 & position) const override;
 };
 
 class MapRendererAdventureTransitionContext : public MapRendererAdventureContext

+ 2 - 1
client/mapView/MapView.cpp

@@ -160,6 +160,7 @@ void MapView::onViewMapActivated()
 PuzzleMapView::PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter)
 	: BasicMapView(offset, dimensions)
 {
-	controller->setViewCenter(tileToCenter);
 	controller->activatePuzzleMapContext(tileToCenter);
+	controller->setViewCenter(tileToCenter);
+
 }

+ 2 - 1
client/mapView/MapViewController.cpp

@@ -63,7 +63,7 @@ void MapViewController::setViewCenter(const Point & position, int level)
 	model->setViewCenter(betterPosition);
 	model->setLevel(vstd::clamp(level, 0, context->getMapSize().z));
 
-	if(adventureInt) // may be called before adventureInt is initialized
+	if(adventureInt && !puzzleMapContext) // may be called before adventureInt is initialized
 		adventureInt->onMapViewMoved(model->getTilesTotalRect(), model->getLevel());
 }
 
@@ -154,6 +154,7 @@ void MapViewController::updateBefore(uint32_t timeDelta)
 		adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool();
 		adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool();
 		adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool();
+		adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool();
 	}
 }
 

+ 4 - 4
client/windows/CSpellWindow.cpp

@@ -509,7 +509,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 		auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero);
 		if(spellCost > owner->myHero->mana) //insufficient mana
 		{
-			owner->myInt->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[206]) % spellCost % owner->myHero->mana));
+			LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[206]) % spellCost % owner->myHero->mana));
 			return;
 		}
 
@@ -529,7 +529,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 		if((combatSpell ^ inCombat) || inCastle)
 		{
 			std::vector<std::shared_ptr<CComponent>> hlp(1, std::make_shared<CComponent>(CComponent::spell, mySpell->id, 0));
-			owner->myInt->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp);
+			LOCPLINT->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp);
 		}
 		else if(combatSpell)
 		{
@@ -544,9 +544,9 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 				std::vector<std::string> texts;
 				problem.getAll(texts);
 				if(!texts.empty())
-					owner->myInt->showInfoDialog(texts.front());
+					LOCPLINT->showInfoDialog(texts.front());
 				else
-					owner->myInt->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem"));
+					LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem"));
 			}
 		}
 		else //adventure spell

+ 1 - 1
cmake_modules/VersionDefinition.cmake

@@ -1,6 +1,6 @@
 set(VCMI_VERSION_MAJOR 1)
 set(VCMI_VERSION_MINOR 2)
-set(VCMI_VERSION_PATCH 0)
+set(VCMI_VERSION_PATCH 1)
 add_definitions(
 	-DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR}
 	-DVCMI_VERSION_MINOR=${VCMI_VERSION_MINOR}

+ 7 - 1
debian/changelog

@@ -1,9 +1,15 @@
+vcmi (1.2.1) jammy; urgency=medium
+
+  * New upstream release
+
+ -- Ivan Savenko <[email protected]>  Fri, 28 Apr 2023 16:00:00 +0200
+
 vcmi (1.2.0) jammy; urgency=medium
 
   * New upstream release
 
  -- Ivan Savenko <[email protected]>  Fri, 14 Apr 2023 16:00:00 +0200
- 
+
 vcmi (1.1.1) jammy; urgency=medium
 
   * New upstream release

+ 1 - 0
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -38,6 +38,7 @@
 	<url type="bugtracker">https://github.com/vcmi/vcmi/issues</url>
 	<url type="faq">https://vcmi.eu/faq/</url>
 	<releases>
+		<release version="1.2.1" date="2023-04-28" />
 		<release version="1.2.0" date="2023-04-14" />
 		<release version="1.1.1" date="2023-02-03" />
 		<release version="1.1.0" date="2022-12-23" />

+ 1 - 1
launcher/firstLaunch/firstlaunch_moc.cpp

@@ -190,7 +190,7 @@ void FirstLaunchView::heroesDataMissing()
 	ui->labelDataCopy->setVisible(true);
 
 	ui->labelDataFound->setVisible(false);
-	ui->pushButtonDataNext->setEnabled(true);
+	ui->pushButtonDataNext->setEnabled(false);
 
 	if(hasVCMIBuilderScript)
 	{

+ 5 - 5
launcher/jsonutils.cpp

@@ -96,14 +96,14 @@ JsonNode toJson(QVariant object)
 {
 	JsonNode ret;
 
-	if(object.canConvert<QVariantMap>())
-		ret.Struct() = VariantToMap(object.toMap());
-	else if(object.canConvert<QVariantList>())
-		ret.Vector() = VariantToList(object.toList());
-	else if(object.userType() == QMetaType::QString)
+	if(object.userType() == QMetaType::QString)
 		ret.String() = object.toString().toUtf8().data();
 	else if(object.userType() == QMetaType::Bool)
 		ret.Bool() = object.toBool();
+	else if(object.canConvert<QVariantMap>())
+		ret.Struct() = VariantToMap(object.toMap());
+	else if(object.canConvert<QVariantList>())
+		ret.Vector() = VariantToList(object.toList());
 	else if(object.canConvert<int>())
 		ret.Integer() = object.toInt();
 	else if(object.canConvert<double>())

+ 1 - 1
lib/CStack.cpp

@@ -133,7 +133,7 @@ std::vector<si32> CStack::activeSpells() const
 	CSelector selector = Selector::sourceType()(Bonus::SPELL_EFFECT)
 						 .And(CSelector([](const Bonus * b)->bool
 	{
-		return b->type != Bonus::NONE;
+		return b->type != Bonus::NONE && SpellID(b->sid).toSpell() && !SpellID(b->sid).toSpell()->isAdventure();
 	}));
 
 	TConstBonusListPtr spellEffects = getBonuses(selector, Selector::all, cachingStr.str());

+ 3 - 3
lib/mapObjects/CGMarket.cpp

@@ -279,10 +279,10 @@ void CGBlackMarket::newTurn(CRandomGenerator & rand) const
 {
 	int resetPeriod = VLC->settings()->getInteger(EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD);
 
-	if(resetPeriod == 0) //check if feature changing OH3 behavior is enabled
-		return;
+	bool isFirstDay = cb->getDate(Date::DAY) == 1;
+	bool regularResetTriggered = resetPeriod != 0 && ((cb->getDate(Date::DAY)-1) % resetPeriod) != 0;
 
-	if (((cb->getDate(Date::DAY)-1) % resetPeriod) != 0)
+	if (!isFirstDay && !regularResetTriggered)
 		return;
 
 	SetAvailableArtifacts saa;

+ 3 - 0
mapeditor/inspector/inspector.cpp

@@ -472,6 +472,9 @@ void Inspector::setProperty(CGLighthouse * o, const QString & key, const QVarian
 void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVariant & value)
 {
 	if(!o) return;
+	
+	if(key == "Message")
+		o->message = value.toString().toStdString();
 }
 
 void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value)

+ 6 - 5
server/CGameHandler.cpp

@@ -6600,9 +6600,9 @@ void CGameHandler::runBattle()
 			if(!removeGhosts.changedStacks.empty())
 				sendAndApply(&removeGhosts);
 
-			//check for bad morale => freeze
+			// check for bad morale => freeze
 			int nextStackMorale = next->MoraleVal();
-			if (nextStackMorale < 0)
+			if(!next->hadMorale && !next->waited() && nextStackMorale < 0)
 			{
 				auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
 				size_t diceIndex = std::min<size_t>(diceSize.size()-1, -nextStackMorale);
@@ -6788,12 +6788,13 @@ void CGameHandler::runBattle()
 				{
 					//check for good morale
 					nextStackMorale = next->MoraleVal();
-					if(!next->hadMorale  //only one extra move per turn possible
+					if( !battleResult.get()
+						&& !next->hadMorale
 						&& !next->defending
 						&& !next->waited()
 						&& !next->fear
-						&&  next->alive()
-						&&  nextStackMorale > 0)
+						&& next->alive()
+						&& nextStackMorale > 0)
 					{
 						auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
 						size_t diceIndex = std::min<size_t>(diceSize.size()-1, nextStackMorale);