فهرست منبع

Merge branch 'develop' into develop

Ivan Savenko 1 سال پیش
والد
کامیت
042d3c0265
100فایلهای تغییر یافته به همراه1379 افزوده شده و 519 حذف شده
  1. 78 0
      AI/BattleAI/AttackPossibility.cpp
  2. 4 0
      AI/BattleAI/AttackPossibility.h
  3. 1 1
      AI/BattleAI/BattleAI.cpp
  4. 89 32
      AI/BattleAI/BattleEvaluator.cpp
  5. 9 2
      AI/BattleAI/BattleEvaluator.h
  6. 21 11
      AI/BattleAI/BattleExchangeVariant.cpp
  7. 1 0
      AI/BattleAI/PotentialTargets.cpp
  8. 9 1
      AI/BattleAI/StackWithBonuses.cpp
  9. 2 0
      AI/BattleAI/StackWithBonuses.h
  10. 1 3
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  11. 6 1
      AI/Nullkiller/Goals/RecruitHero.cpp
  12. 1 0
      CMakeLists.txt
  13. 37 3
      Mods/vcmi/config/vcmi/chinese.json
  14. 3 1
      Mods/vcmi/config/vcmi/english.json
  15. 36 2
      Mods/vcmi/config/vcmi/polish.json
  16. 2 2
      client/CMT.h
  17. 27 122
      client/CMakeLists.txt
  18. 1 11
      client/CPlayerInterface.cpp
  19. 10 2
      client/CServerHandler.cpp
  20. 1 1
      client/Client.h
  21. 10 1
      client/ClientCommandManager.cpp
  22. 3 0
      client/ClientCommandManager.h
  23. 1 1
      client/ClientNetPackVisitors.h
  24. 11 2
      client/NetPacksClient.cpp
  25. 5 1
      client/NetPacksLobbyClient.cpp
  26. 1 1
      client/battle/BattleAnimationClasses.cpp
  27. 1 1
      client/battle/BattleInterface.cpp
  28. 1 1
      client/battle/BattleProjectileController.cpp
  29. 37 30
      client/battle/BattleSiegeController.cpp
  30. 1 1
      client/battle/BattleSiegeController.h
  31. 1 1
      client/battle/BattleStacksController.cpp
  32. 6 1
      client/eventsSDL/InputSourceMouse.cpp
  33. 0 1
      client/eventsSDL/InputSourceText.cpp
  34. 0 1
      client/eventsSDL/InputSourceTouch.cpp
  35. 39 1
      client/lobby/CBonusSelection.cpp
  36. 17 0
      client/lobby/CBonusSelection.h
  37. 3 0
      client/lobby/OptionsTabBase.cpp
  38. 6 0
      client/lobby/RandomMapTab.cpp
  39. 2 0
      client/lobby/SelectionTab.cpp
  40. 21 15
      client/mainmenu/CMainMenu.cpp
  41. 1 0
      client/mainmenu/CMainMenu.h
  42. 1 0
      client/mainmenu/CStatisticScreen.cpp
  43. 116 0
      client/render/AssetGenerator.cpp
  44. 18 0
      client/render/AssetGenerator.h
  45. 2 6
      client/render/ImageLocator.cpp
  46. 12 4
      client/render/ImageLocator.h
  47. 14 2
      client/renderSDL/CBitmapFont.cpp
  48. 11 5
      client/renderSDL/CTrueTypeFont.cpp
  49. 2 1
      client/renderSDL/CTrueTypeFont.h
  50. 8 10
      client/renderSDL/ImageScaled.cpp
  51. 5 7
      client/renderSDL/RenderHandler.cpp
  52. 7 26
      client/renderSDL/SDLImage.cpp
  53. 20 7
      client/renderSDL/SDL_Extensions.cpp
  54. 8 1
      client/renderSDL/SDL_Extensions.h
  55. 101 90
      client/windows/CCastleInterface.cpp
  56. 3 2
      client/windows/CCastleInterface.h
  57. 2 4
      client/windows/CKingdomInterface.cpp
  58. 11 5
      client/windows/CMessage.cpp
  59. 3 54
      client/windows/CSpellWindow.cpp
  60. 0 2
      client/windows/CSpellWindow.h
  61. 2 3
      client/windows/GUIClasses.cpp
  62. 1 1
      client/windows/GUIClasses.h
  63. 2 2
      client/windows/InfoWindows.cpp
  64. 1 1
      clientapp/CFocusableHelper.cpp
  65. 0 0
      clientapp/CFocusableHelper.h
  66. 137 0
      clientapp/CMakeLists.txt
  67. 30 28
      clientapp/EntryPoint.cpp
  68. 11 0
      clientapp/StdInc.cpp
  69. 14 0
      clientapp/StdInc.h
  70. 0 0
      clientapp/VCMI_client.rc
  71. 0 0
      clientapp/ios/GameChatKeyboardHandler.h
  72. 0 0
      clientapp/ios/GameChatKeyboardHandler.m
  73. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/Contents.json
  74. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  75. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  76. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  77. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  78. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  79. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  80. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  81. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  82. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  83. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  84. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  85. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  86. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  87. 0 0
      clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  88. 0 0
      clientapp/ios/Images.xcassets/Contents.json
  89. 0 0
      clientapp/ios/Info.plist
  90. 0 0
      clientapp/ios/LaunchScreen.storyboard
  91. 0 0
      clientapp/ios/Settings.bundle/Root.plist
  92. 0 0
      clientapp/ios/Settings.bundle/en.lproj/Root.strings
  93. 0 0
      clientapp/ios/Settings.bundle/ru.lproj/Root.strings
  94. 0 0
      clientapp/ios/main.m
  95. 0 0
      clientapp/ios/startSDL.h
  96. 0 0
      clientapp/ios/startSDL.mm
  97. 0 0
      clientapp/ios/vcmi_logo.png
  98. 9 0
      config/bonuses.json
  99. 68 4
      config/buildingsLibrary.json
  100. 254 0
      config/campaignOverrides.json

+ 78 - 0
AI/BattleAI/AttackPossibility.cpp

@@ -12,6 +12,10 @@
 #include "../../lib/CStack.h" // TODO: remove
                               // Eventually only IBattleInfoCallback and battle::Unit should be used, 
                               // CUnitState should be private and CStack should be removed completely
+#include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/spells/ISpellMechanics.h"
+#include "../../lib/spells/ObstacleCasterProxy.h"
+#include "../../lib/battle/CObstacleInstance.h"
 
 uint64_t averageDmg(const DamageRange & range)
 {
@@ -25,9 +29,57 @@ void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit
 	damageCache[attacker->unitId()][defender->unitId()] = static_cast<float>(damage) / attacker->getCount();
 }
 
+void DamageCache::buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side)
+{
+	for(const auto & obst : hb->battleGetAllObstacles(side))
+	{
+		auto spellObstacle = dynamic_cast<const SpellCreatedObstacle *>(obst.get());
+
+		if(!spellObstacle || !obst->triggersEffects())
+			continue;
+
+		auto triggerAbility = VLC->spells()->getById(obst->getTrigger());
+		auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage();
+
+		if(!triggerIsNegative)
+			continue;
+
+		const auto * hero = hb->battleGetFightingHero(spellObstacle->casterSide);
+		auto caster = spells::ObstacleCasterProxy(hb->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle);
+
+		auto affectedHexes = obst->getAffectedTiles();
+		auto stacks = hb->battleGetUnitsIf([](const battle::Unit * u) -> bool {
+			return u->alive() && !u->isTurret() && u->getPosition().isValid();
+		});
+
+		for(auto stack : stacks)
+		{
+			std::shared_ptr<HypotheticBattle> inner = std::make_shared<HypotheticBattle>(hb->env, hb);
+			auto cast = spells::BattleCast(hb.get(), &caster, spells::Mode::PASSIVE, obst->getTrigger().toSpell());
+			auto updated = inner->getForUpdate(stack->unitId());
+
+			spells::Target target;
+			target.push_back(spells::Destination(updated.get()));
+
+			cast.castEval(inner->getServerCallback(), target);
+
+			auto damageDealt = stack->getAvailableHealth() - updated->getAvailableHealth();
+
+			for(auto hex : affectedHexes)
+			{
+				obstacleDamage[hex][stack->unitId()] = damageDealt;
+			}
+		}
+	}
+}
 
 void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side)
 {
+	if(parent == nullptr)
+	{
+		buildObstacleDamageCache(hb, side);
+	}
+
 	auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool
 		{
 			return u->isValidTarget();
@@ -70,6 +122,23 @@ int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit
 	return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
 }
 
+int64_t DamageCache::getObstacleDamage(BattleHex hex, const battle::Unit * defender)
+{
+	if(parent)
+		return parent->getObstacleDamage(hex, defender);
+
+	auto damages = obstacleDamage.find(hex);
+
+	if(damages == obstacleDamage.end())
+		return 0;
+
+	auto damage = damages->second.find(defender->unitId());
+
+	return damage == damages->second.end()
+		? 0
+		: damage->second;
+}
+
 int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
 {
 	if(parent)
@@ -288,6 +357,15 @@ AttackPossibility AttackPossibility::evaluate(
 			{
 				retaliatedUnits.push_back(attacker);
 			}
+
+			auto obstacleDamage = damageCache.getObstacleDamage(hex, attacker);
+
+			if(obstacleDamage > 0)
+			{
+				ap.attackerDamageReduce += calculateDamageReduce(nullptr, attacker, obstacleDamage, damageCache, state);
+
+				ap.attackerState->damage(obstacleDamage);
+			}
 		}
 
 		// ensure the defender is also affected

+ 4 - 0
AI/BattleAI/AttackPossibility.h

@@ -18,14 +18,18 @@ class DamageCache
 {
 private:
 	std::unordered_map<uint32_t, std::unordered_map<uint32_t, float>> damageCache;
+	std::map<BattleHex, std::unordered_map<uint32_t, int64_t>> obstacleDamage;
 	DamageCache * parent;
 
+	void buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side);
+
 public:
 	DamageCache() : parent(nullptr) {}
 	DamageCache(DamageCache * parent) : parent(parent) {}
 
 	void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
 	int64_t getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
+	int64_t getObstacleDamage(BattleHex hex, const battle::Unit * defender);
 	int64_t getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
 	void buildDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side);
 };

+ 1 - 1
AI/BattleAI/BattleAI.cpp

@@ -229,7 +229,7 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st
 		{
 			auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart);
 
-			if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
+			if(wallState != EWallState::NONE && wallState != EWallState::DESTROYED)
 			{
 				targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart);
 				break;

+ 89 - 32
AI/BattleAI/BattleEvaluator.cpp

@@ -17,6 +17,7 @@
 #include "../../lib/CStopWatch.h"
 #include "../../lib/CThreadHelper.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/battle/BattleStateInfoForRetreat.h"
@@ -66,7 +67,6 @@ BattleEvaluator::BattleEvaluator(
 	damageCache.buildDamageCache(hb, side);
 
 	targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
-	cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
 }
 
 BattleEvaluator::BattleEvaluator(
@@ -85,7 +85,6 @@ BattleEvaluator::BattleEvaluator(
 	damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount)
 {
 	targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
-	cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
 }
 
 std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
@@ -178,8 +177,10 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
 		auto & bestAttack = evaluationResult.bestAttack;
 
-		cachedAttack = bestAttack;
-		cachedScore = evaluationResult.score;
+		cachedAttack.ap = bestAttack;
+		cachedAttack.score = evaluationResult.score;
+		cachedAttack.turn = 0;
+		cachedAttack.waited = evaluationResult.wait;
 
 		//TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc.
 		if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff())
@@ -225,11 +226,36 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 					{
 						return BattleAction::makeDefend(stack);
 					}
-					else
+
+					auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool
+						{
+							return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
+						});
+
+					bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4;
+					bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
+						&& !bestAttack.attack.shooting
+						&& hb->battleGetFortifications().hasMoat
+						&& !enemyMellee.empty()
+						&& isTargetOutsideFort;
+
+					if(siegeDefense)
 					{
-						activeActionMade = true;
-						return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from);
+						logAi->trace("Evaluating exchange at %d self-defense", stack->getPosition().hex);
+
+						BattleAttackInfo bai(stack, stack, 0, false);
+						AttackPossibility apDefend(stack->getPosition(), stack->getPosition(), bai);
+
+						float defenseValue = scoreEvaluator.evaluateExchange(apDefend, 0, *targets, damageCache, hb);
+
+						if((defenseValue > score && score <= 0) || (defenseValue > 2 * score && score > 0))
+						{
+							return BattleAction::makeDefend(stack);
+						}
 					}
+					
+					activeActionMade = true;
+					return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from);
 				}
 			}
 		}
@@ -239,8 +265,9 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 	if(moveTarget.score > score)
 	{
 		score = moveTarget.score;
-		cachedAttack = moveTarget.cachedAttack;
-		cachedScore = score;
+		cachedAttack.ap = moveTarget.cachedAttack;
+		cachedAttack.score = score;
+		cachedAttack.turn = moveTarget.turnsToRich;
 
 		if(stack->waited())
 		{
@@ -255,6 +282,8 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		}
 		else
 		{
+			cachedAttack.waited = true;
+
 			return BattleAction::makeWait(stack);
 		}
 	}
@@ -262,7 +291,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 	if(score <= EvaluationResult::INEFFECTIVE_SCORE
 		&& !stack->hasBonusOfType(BonusType::FLYING)
 		&& stack->unitSide() == BattleSide::ATTACKER
-		&& cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
+	   && cb->getBattle(battleID)->battleGetFortifications().hasMoat)
 	{
 		auto brokenWallMoat = getBrokenWallMoatHexes();
 
@@ -448,7 +477,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 	vstd::erase_if(possibleSpells, [](const CSpell *s)
 	{
-		return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION;
+		return spellType(s) != SpellTypes::BATTLE;
 	});
 
 	LOGFL("I know how %d of them works.", possibleSpells.size());
@@ -459,9 +488,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 	{
 		spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell);
 
-		if(spell->getTargetType() == spells::AimType::LOCATION)
-			continue;
-		
 		const bool FAST = true;
 
 		for(auto & target : temp.findPotentialTargets(FAST))
@@ -630,7 +656,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				auto & ps = possibleCasts[i];
 
 #if BATTLE_TRACE_LEVEL >= 1
-				logAi->trace("Evaluating %s", ps.spell->getNameTranslated());
+				if(ps.dest.empty())
+					logAi->trace("Evaluating %s", ps.spell->getNameTranslated());
+				else
+				{
+					auto psFirst = ps.dest.front();
+					auto strWhere = psFirst.unitValue ? psFirst.unitValue->getDescription() : std::to_string(psFirst.hexValue.hex);
+
+					logAi->trace("Evaluating %s at %s", ps.spell->getNameTranslated(), strWhere);
+				}
 #endif
 
 				auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
@@ -648,9 +682,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 				DamageCache safeCopy = damageCache;
 				DamageCache innerCache(&safeCopy);
+
 				innerCache.buildDamageCache(state, side);
 
-				if(needFullEval || !cachedAttack)
+				if(cachedAttack.ap && cachedAttack.waited)
+				{
+					state->makeWait(activeStack);
+				}
+
+				if(needFullEval || !cachedAttack.ap)
 				{
 #if BATTLE_TRACE_LEVEL >= 1
 					logAi->trace("Full evaluation is started due to stack speed affected.");
@@ -659,29 +699,41 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 					PotentialTargets innerTargets(activeStack, innerCache, state);
 					BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount);
 
+					innerEvaluator.updateReachabilityMap(state);
+
+					auto moveTarget = innerEvaluator.findMoveTowardsUnreachable(activeStack, innerTargets, innerCache, state);
+
 					if(!innerTargets.possibleAttacks.empty())
 					{
-						innerEvaluator.updateReachabilityMap(state);
-
 						auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state);
 
-						ps.value = newStackAction.score;
+						ps.value = std::max(moveTarget.score, newStackAction.score);
 					}
 					else
 					{
-						ps.value = 0;
+						ps.value = moveTarget.score;
 					}
 				}
 				else
 				{
-					ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state);
+					auto updatedAttacker = state->getForUpdate(cachedAttack.ap->attack.attacker->unitId());
+					auto updatedDefender = state->getForUpdate(cachedAttack.ap->attack.defender->unitId());
+					auto updatedBai = BattleAttackInfo(
+						updatedAttacker.get(),
+						updatedDefender.get(),
+						cachedAttack.ap->attack.chargeDistance,
+						cachedAttack.ap->attack.shooting);
+
+					auto updatedAttack = AttackPossibility::evaluate(updatedBai, cachedAttack.ap->from, innerCache, state);
+
+					ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
 				}
 
 				for(const auto & unit : allUnits)
 				{
-					if (!unit->isValidTarget())
+					if(!unit->isValidTarget(true))
 						continue;
-					
+
 					auto newHealth = unit->getAvailableHealth();
 					auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units
 
@@ -692,7 +744,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 						auto dpsReduce = AttackPossibility::calculateDamageReduce(
 							nullptr,
-							originalDefender &&  originalDefender->alive() ? originalDefender : unit,
+							originalDefender && originalDefender->alive() ? originalDefender : unit,
 							damage,
 							innerCache,
 							state);
@@ -702,13 +754,18 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 						if(ourUnit * goodEffect == 1)
 						{
-							if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost()))
+							auto isMagical = state->getForUpdate(unit->unitId())->summoned
+								|| unit->isClone()
+								|| unit->isGhost();
+
+							if(ourUnit && goodEffect && isMagical)
 								continue;
 
 							ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
 						}
 						else
-							ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
+							// discourage AI making collateral damage with spells
+							ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
 
 #if BATTLE_TRACE_LEVEL >= 1
 						logAi->trace(
@@ -719,6 +776,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 #endif
 					}
 				}
+
 #if BATTLE_TRACE_LEVEL >= 1
 				logAi->trace("Total score: %2f", ps.value);
 #endif
@@ -729,13 +787,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 	LOGFL("Evaluation took %d ms", timer.getDiff());
 
-	auto pscValue = [](const PossibleSpellcast &ps) -> float
-	{
-		return ps.value;
-	};
-	auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue);
+	auto castToPerform = *vstd::maxElementByFun(possibleCasts, [](const PossibleSpellcast & ps) -> float
+		{
+			return ps.value;
+		});
 
-	if(castToPerform.value > cachedScore)
+	if(castToPerform.value > cachedAttack.score && !vstd::isAlmostEqual(castToPerform.value, cachedAttack.score))
 	{
 		LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value);
 		BattleAction spellcast;

+ 9 - 2
AI/BattleAI/BattleEvaluator.h

@@ -22,6 +22,14 @@ VCMI_LIB_NAMESPACE_END
 
 class EnemyInfo;
 
+struct CachedAttack
+{
+	std::optional<AttackPossibility> ap;
+	float score = EvaluationResult::INEFFECTIVE_SCORE;
+	uint8_t turn = 255;
+	bool waited = false;
+};
+
 class BattleEvaluator
 {
 	std::unique_ptr<PotentialTargets> targets;
@@ -30,11 +38,10 @@ class BattleEvaluator
 	std::shared_ptr<CBattleCallback> cb;
 	std::shared_ptr<Environment> env;
 	bool activeActionMade = false;
-	std::optional<AttackPossibility> cachedAttack;
+	CachedAttack cachedAttack;
 	PlayerColor playerID;
 	BattleID battleID;
 	BattleSide side;
-	float cachedScore;
 	DamageCache damageCache;
 	float strengthRatio;
 	int simulationTurnsCount;

+ 21 - 11
AI/BattleAI/BattleExchangeVariant.cpp

@@ -28,6 +28,12 @@ float BattleExchangeVariant::trackAttack(
 	std::shared_ptr<HypotheticBattle> hb,
 	DamageCache & damageCache)
 {
+	if(!ap.attackerState)
+	{
+		logAi->trace("Skipping fake ap attack");
+		return 0;
+	}
+
 	auto attacker = hb->getForUpdate(ap.attack.attacker->unitId());
 
 	float attackValue = ap.attackValue();
@@ -219,9 +225,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 		auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
 
-		hbWaited->resetActiveUnit();
-		hbWaited->getForUpdate(activeStack->unitId())->waiting = true;
-		hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true;
+		hbWaited->makeWait(activeStack);
 
 		updateReachabilityMap(hbWaited);
 
@@ -378,11 +382,14 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 				logAi->trace("New high score");
 #endif
 
-				for(BattleHex enemyHex : enemy->getAttackableHexes(activeStack))
+				for(const BattleHex & initialEnemyHex : enemy->getAttackableHexes(activeStack))
 				{
-					while(!flying && dists.distances[enemyHex] > speed)
+					BattleHex enemyHex = initialEnemyHex;
+
+					while(!flying && dists.distances[enemyHex] > speed && dists.predecessors.at(enemyHex).isValid())
 					{
 						enemyHex = dists.predecessors.at(enemyHex);
+
 						if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK)
 						{
 							auto defenderToBypass = hb->battleGetUnitByPos(enemyHex);
@@ -484,15 +491,18 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
 		vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex));
 	}
 
-	for(auto hex : ap.attack.attacker->getHexes())
+	if(!ap.attack.attacker->isTurret())
 	{
-		auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex);
-		for(auto unit : unitsReachingAttacker)
+		for(auto hex : ap.attack.attacker->getHexes())
 		{
-			if(unit->unitSide() != ap.attack.attacker->unitSide())
+			auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex);
+			for(auto unit : unitsReachingAttacker)
 			{
-				allReachableUnits.push_back(unit);
-				result.enemyUnitsReachingAttacker.insert(unit->unitId());
+				if(unit->unitSide() != ap.attack.attacker->unitSide())
+				{
+					allReachableUnits.push_back(unit);
+					result.enemyUnitsReachingAttacker.insert(unit->unitId());
+				}
 			}
 		}
 	}

+ 1 - 0
AI/BattleAI/PotentialTargets.cpp

@@ -10,6 +10,7 @@
 #include "StdInc.h"
 #include "PotentialTargets.h"
 #include "../../lib/CStack.h"//todo: remove
+#include "../../lib/mapObjects/CGTownInstance.h"
 
 PotentialTargets::PotentialTargets(
 	const battle::Unit * attacker,

+ 9 - 1
AI/BattleAI/StackWithBonuses.cpp

@@ -502,10 +502,18 @@ ServerCallback * HypotheticBattle::getServerCallback()
 	return serverCallback.get();
 }
 
+void HypotheticBattle::makeWait(const battle::Unit * activeStack)
+{
+	auto unit = getForUpdate(activeStack->unitId());
+
+	resetActiveUnit();
+	unit->waiting = true;
+	unit->waitedThisTurn = true;
+}
+
 HypotheticBattle::HypotheticServerCallback::HypotheticServerCallback(HypotheticBattle * owner_)
 	:owner(owner_)
 {
-
 }
 
 void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem)

+ 2 - 0
AI/BattleAI/StackWithBonuses.h

@@ -164,6 +164,8 @@ public:
 
 	int64_t getTreeVersion() const;
 
+	void makeWait(const battle::Unit * activeStack);
+
 	void resetActiveUnit()
 	{
 		activeUnitId = -1;

+ 1 - 3
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -314,9 +314,7 @@ void BuildAnalyzer::updateDailyIncome()
 		const CGMine* mine = dynamic_cast<const CGMine*>(obj);
 
 		if(mine)
-		{
-			dailyIncome[mine->producedResource.getNum()] += mine->getProducedQuantity();
-		}
+			dailyIncome += mine->dailyIncome();
 	}
 
 	for(const CGTownInstance* town : towns)

+ 6 - 1
AI/Nullkiller/Goals/RecruitHero.cpp

@@ -68,7 +68,12 @@ void RecruitHero::accept(AIGateway * ai)
 		throw cannotFulfillGoalException("Town " + t->nodeName() + " is occupied. Cannot recruit hero!");
 
 	cb->recruitHero(t, heroToHire);
-	ai->nullkiller->heroManager->update();
+
+	{
+		std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
+
+		ai->nullkiller->heroManager->update();
+	}
 
 	if(t->visitingHero)
 		ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get());

+ 1 - 0
CMakeLists.txt

@@ -680,6 +680,7 @@ endif()
 
 if (ENABLE_CLIENT)
 	add_subdirectory(client)
+	add_subdirectory(clientapp)
 endif()
 
 if(ENABLE_SERVER)

+ 37 - 3
Mods/vcmi/config/vcmi/chinese.json

@@ -54,9 +54,9 @@
 	"vcmi.radialWheel.moveUp" : "上移",
 	"vcmi.radialWheel.moveDown" : "下移",
 	"vcmi.radialWheel.moveBottom" : "移到底端",
-	
+
 	"vcmi.spellBook.search" : "搜索中...",
-	
+
 	"vcmi.mainMenu.serverConnecting" : "连接中...",
 	"vcmi.mainMenu.serverAddressEnter" : "使用地址:",
 	"vcmi.mainMenu.serverConnectionFailed" : "连接失败",
@@ -162,6 +162,38 @@
 	"vcmi.systemOptions.otherGroup" : "其他设置", // unused right now
 	"vcmi.systemOptions.townsGroup" : "城镇画面",
 
+	"vcmi.statisticWindow.statistics" : "统计",
+	"vcmi.statisticWindow.tsvCopy" : "复制到剪切板",
+	"vcmi.statisticWindow.selectView" : "选择视角",
+	"vcmi.statisticWindow.value" : "值",
+	"vcmi.statisticWindow.title.overview" : "概况",
+	"vcmi.statisticWindow.title.resources" : "资源",
+	"vcmi.statisticWindow.title.income" : "收入",
+	"vcmi.statisticWindow.title.numberOfHeroes" : "英雄数量",
+	"vcmi.statisticWindow.title.numberOfTowns" : "城镇数量",
+	"vcmi.statisticWindow.title.numberOfArtifacts" : "宝物数量",
+	"vcmi.statisticWindow.title.numberOfDwellings" : "野外巢穴数量",
+	"vcmi.statisticWindow.title.numberOfMines" : "矿井数量",
+	"vcmi.statisticWindow.title.armyStrength" : "部队强度",
+	"vcmi.statisticWindow.title.experience" : "经验",
+	"vcmi.statisticWindow.title.resourcesSpentArmy" : "部队花费",
+	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "建造花费",
+	"vcmi.statisticWindow.title.mapExplored" : "地图探索比例",
+	"vcmi.statisticWindow.param.playerName" : "玩家名称",
+	"vcmi.statisticWindow.param.daysSurvived" : "存活天数",
+	"vcmi.statisticWindow.param.maxHeroLevel" : "最大英雄等级",
+	"vcmi.statisticWindow.param.battleWinRatioHero" : "胜率(对英雄)",
+	"vcmi.statisticWindow.param.battleWinRatioNeutral" : "胜率(对中立生物)",
+	"vcmi.statisticWindow.param.battlesHero" : "战斗(对英雄)",
+	"vcmi.statisticWindow.param.battlesNeutral" : "战斗(对中立生物)",
+	"vcmi.statisticWindow.param.maxArmyStrength" : "最大部队强度",
+	"vcmi.statisticWindow.param.tradeVolume" : "交易量",
+	"vcmi.statisticWindow.param.obeliskVisited" : "方尖塔访问",
+	"vcmi.statisticWindow.icon.townCaptured" : "占领城镇",
+	"vcmi.statisticWindow.icon.strongestHeroDefeated" : "击败对手最强英雄",
+	"vcmi.statisticWindow.icon.grailFound" : "找到神器",
+	"vcmi.statisticWindow.icon.defeated" : "被击败",
+
 	"vcmi.systemOptions.fullscreenBorderless.hover" : "全屏 (无边框)",
 	"vcmi.systemOptions.fullscreenBorderless.help"  : "{全屏}\n\n选中时,VCMI将以无边框全屏模式运行。该模式下,游戏会始终和桌面分辨率保持一致,无视设置的分辨率。 ",
 	"vcmi.systemOptions.fullscreenExclusive.hover"  : "全屏 (独占)",
@@ -628,5 +660,7 @@
 	"core.bonus.WATER_IMMUNITY.name": "水系免疫",
 	"core.bonus.WATER_IMMUNITY.description": "免疫水系魔法",
 	"core.bonus.WIDE_BREATH.name": "弧形焰息",
-	"core.bonus.WIDE_BREATH.description": "大范围喷吐攻击(目标左右以及后方共6格)"
+	"core.bonus.WIDE_BREATH.description": "大范围喷吐攻击(目标左右以及后方共6格)",
+	"core.bonus.DISINTEGRATE.name": "解体",
+	"core.bonus.DISINTEGRATE.description": "死亡后不会留下尸体"
 }

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

@@ -660,5 +660,7 @@
 	"core.bonus.WATER_IMMUNITY.name": "Water immunity",
 	"core.bonus.WATER_IMMUNITY.description": "Immune to all spells from the school of Water magic",
 	"core.bonus.WIDE_BREATH.name": "Wide breath",
-	"core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)"
+	"core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)",
+	"core.bonus.DISINTEGRATE.name": "Disintegrate",
+	"core.bonus.DISINTEGRATE.description": "No corpse remains after death"
 }

+ 36 - 2
Mods/vcmi/config/vcmi/polish.json

@@ -140,10 +140,10 @@
 
 	"vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.",
+	"vcmi.server.errors.disconnected" : "{Błąd sieciowy}\n\nUtracono połączenie z serwerem!",
 	"vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",
 	"vcmi.server.errors.modsToEnable"    : "{Następujące mody są wymagane do wczytania gry}",
 	"vcmi.server.errors.modsToDisable"   : "{Następujące mody muszą zostać wyłączone}",
-	"vcmi.server.confirmReconnect"       : "Połączyć ponownie z ostatnią sesją?",
 	"vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n",
 	"vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n",
 	"vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!",
@@ -162,6 +162,38 @@
 	"vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now
 	"vcmi.systemOptions.townsGroup" : "Ekran miasta",
 
+	"vcmi.statisticWindow.statistics" : "Statystyki",
+	"vcmi.statisticWindow.tsvCopy" : "Kopiuj do schowka",
+	"vcmi.statisticWindow.selectView" : "Tryb widoku",
+	"vcmi.statisticWindow.value" : "Wartość",
+	"vcmi.statisticWindow.title.overview" : "Przegląd",
+	"vcmi.statisticWindow.title.resources" : "Surowce",
+	"vcmi.statisticWindow.title.income" : "Przychód",
+	"vcmi.statisticWindow.title.numberOfHeroes" : "Lb. bohaterów",
+	"vcmi.statisticWindow.title.numberOfTowns" : "Lb. miast",
+	"vcmi.statisticWindow.title.numberOfArtifacts" : "Lb. artefaktów",
+	"vcmi.statisticWindow.title.numberOfDwellings" : "Lb. siedlisk",
+	"vcmi.statisticWindow.title.numberOfMines" : "Lb. kopalni",
+	"vcmi.statisticWindow.title.armyStrength" : "Siła armii",
+	"vcmi.statisticWindow.title.experience" : "Doświadczenie",
+	"vcmi.statisticWindow.title.resourcesSpentArmy" : "Koszty armii",
+	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Koszty budowy",
+	"vcmi.statisticWindow.title.mapExplored" : "Odkrycie mapy",
+	"vcmi.statisticWindow.param.playerName" : "Imię",
+	"vcmi.statisticWindow.param.daysSurvived" : "Przeżytych dni",
+	"vcmi.statisticWindow.param.maxHeroLevel" : "Maks. poziom bohatera",
+	"vcmi.statisticWindow.param.battleWinRatioHero" : "Zwycięstw (vs. bohaterom)",
+	"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Zwycięstw (vs. neutralnym)",
+	"vcmi.statisticWindow.param.battlesHero" : "Walk (vs. bohaterom)",
+	"vcmi.statisticWindow.param.battlesNeutral" : "Walk (vs. neutralnym)",
+	"vcmi.statisticWindow.param.maxArmyStrength" : "Maks. siła armii",
+	"vcmi.statisticWindow.param.tradeVolume" : "Wielkość handlu",
+	"vcmi.statisticWindow.param.obeliskVisited" : "Lb. obelisków",
+	"vcmi.statisticWindow.icon.townCaptured" : "Miasto zdobyte",
+	"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Najsilniejszy bohater przeciwnika pokonany",
+	"vcmi.statisticWindow.icon.grailFound" : "Gral znaleziony",
+	"vcmi.statisticWindow.icon.defeated" : "Pokonany",
+
 	"vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)",
 	"vcmi.systemOptions.fullscreenBorderless.help"  : "{Pełny ekran w trybie okna}\n\nVCMI będzie działać w trybie okna pełnoekranowego. W tym trybie gra będzie zawsze używać rozdzielczości pulpitu, ignorując wybraną rozdzielczość.",
 	"vcmi.systemOptions.fullscreenExclusive.hover"  : "Pełny ekran (tradycyjny)",
@@ -628,5 +660,7 @@
 	"core.bonus.WATER_IMMUNITY.name": "Odporność: Woda",
 	"core.bonus.WATER_IMMUNITY.description": "Odporny na wszystkie czary szkoły wody",
 	"core.bonus.WIDE_BREATH.name": "Szerokie zionięcie",
-	"core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)"
+	"core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)",
+	"core.bonus.DISINTEGRATE.name": "Rozpadanie",
+	"core.bonus.DISINTEGRATE.description": "Po śmierci nie pozostaje żaden trup"
 }

+ 2 - 2
client/CMT.h

@@ -20,8 +20,8 @@ extern SDL_Surface *screen;      // main screen surface
 extern SDL_Surface *screen2;     // and hlp surface (used to store not-active interfaces layer)
 extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed
 
-void handleQuit(bool ask = true);
-
 /// Notify user about encountered fatal error and terminate the game
+/// Defined in clientapp EntryPoint
 /// TODO: decide on better location for this method
 [[noreturn]] void handleFatalError(const std::string & message, bool terminate);
+void handleQuit(bool ask = true);

+ 27 - 122
client/CMakeLists.txt

@@ -1,4 +1,4 @@
-set(client_SRCS
+set(vcmiclientcommon_SRCS
 	StdInc.cpp
 	../CCallback.cpp
 
@@ -83,6 +83,7 @@ set(client_SRCS
 	media/CSoundHandler.cpp
 	media/CVideoHandler.cpp
 
+	render/AssetGenerator.cpp
 	render/CAnimation.cpp
 	render/CBitmapHandler.cpp
 	render/CDefFile.cpp
@@ -177,7 +178,6 @@ set(client_SRCS
 
 	ArtifactsUIController.cpp
 	CGameInfo.cpp
-	CMT.cpp
 	CPlayerInterface.cpp
 	PlayerLocalState.cpp
 	CServerHandler.cpp
@@ -190,7 +190,7 @@ set(client_SRCS
 	ServerRunner.cpp
 )
 
-set(client_HEADERS
+set(vcmiclientcommon_HEADERS
 	StdInc.h
 
 	adventureMap/AdventureMapInterface.h
@@ -285,6 +285,7 @@ set(client_HEADERS
 	media/ISoundPlayer.h
 	media/IVideoPlayer.h
 
+	render/AssetGenerator.h
 	render/CAnimation.h
 	render/CBitmapHandler.h
 	render/CDefFile.h
@@ -405,76 +406,50 @@ set(client_HEADERS
 )
 
 if(APPLE_IOS)
-	set(client_SRCS ${client_SRCS}
-		CFocusableHelper.cpp
-		ios/GameChatKeyboardHandler.m
-		ios/main.m
-		ios/startSDL.mm
+	set(vcmiclientcommon_SRCS ${vcmiclientcommon_SRCS}
 		ios/utils.mm
 	)
-	set(client_HEADERS ${client_HEADERS}
-		CFocusableHelper.h
-		ios/GameChatKeyboardHandler.h
-		ios/startSDL.h
+	set(vcmiclientcommon_HEADERS ${vcmiclientcommon_HEADERS}
 		ios/utils.h
 	)
 endif()
 
-assign_source_group(${client_SRCS} ${client_HEADERS} VCMI_client.rc)
-
-if(ANDROID)
-	add_library(vcmiclient SHARED ${client_SRCS} ${client_HEADERS})
-	set_target_properties(vcmiclient PROPERTIES
-		OUTPUT_NAME "vcmiclient_${ANDROID_ABI}" # required by Qt
-	)
-else()
-	add_executable(vcmiclient ${client_SRCS} ${client_HEADERS})
-endif()
+assign_source_group(${vcmiclientcommon_SRCS} ${vcmiclientcommon_HEADERS})
+add_library(vcmiclientcommon STATIC ${vcmiclientcommon_SRCS} ${vcmiclientcommon_HEADERS})
 
 if(NOT ENABLE_STATIC_LIBS)
-	add_dependencies(vcmiclient
+	add_dependencies(vcmiclientcommon
 		BattleAI
 		EmptyAI
 		StupidAI
 		VCAI
 	)
 	if(ENABLE_NULLKILLER_AI)
-		add_dependencies(vcmiclient Nullkiller)
+		add_dependencies(vcmiclientcommon Nullkiller)
 	endif()
 endif()
 if(APPLE_IOS)
 	if(ENABLE_ERM)
-		add_dependencies(vcmiclient vcmiERM)
+		add_dependencies(vcmiclientcommon vcmiERM)
 	endif()
 	if(ENABLE_LUA)
-		add_dependencies(vcmiclient vcmiLua)
+		add_dependencies(vcmiclientcommon vcmiLua)
 	endif()
 endif()
 
 if(WIN32)
-	target_sources(vcmiclient PRIVATE "VCMI_client.rc")
-	set_target_properties(vcmiclient
+	set_target_properties(vcmiclientcommon
 		PROPERTIES
-			OUTPUT_NAME "VCMI_client"
-			PROJECT_LABEL "VCMI_client"
+			OUTPUT_NAME "VCMI_vcmiclientcommon"
+			PROJECT_LABEL "VCMI_vcmiclientcommon"
 	)
-	set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclient)
+	set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclientcommon)
 	if(NOT ENABLE_DEBUG_CONSOLE)
-		set_target_properties(vcmiclient PROPERTIES WIN32_EXECUTABLE)
-		target_link_libraries(vcmiclient SDL2::SDL2main)
-	endif()
-	target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
-
-# TODO: very hacky, find proper solution to copy AI dlls into bin dir
-	if(MSVC)
-		add_custom_command(TARGET vcmiclient POST_BUILD
-			WORKING_DIRECTORY "$<TARGET_FILE_DIR:vcmiclient>"
-			COMMAND ${CMAKE_COMMAND} -E copy AI/fuzzylite.dll fuzzylite.dll
-			COMMAND ${CMAKE_COMMAND} -E copy AI/tbb12.dll tbb12.dll
-		)
+		target_link_libraries(vcmiclientcommon SDL2::SDL2main)
 	endif()
+	target_compile_definitions(vcmiclientcommon PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
 elseif(APPLE_IOS)
-	target_link_libraries(vcmiclient PRIVATE
+	target_link_libraries(vcmiclientcommon PRIVATE
 		iOS_utils
 
 		# FFmpeg
@@ -486,101 +461,31 @@ elseif(APPLE_IOS)
 		"-framework CoreMedia"
 		"-framework VideoToolbox"
 	)
-
-	set_target_properties(vcmiclient PROPERTIES
-		MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist"
-		XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
-		XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "$(CODE_SIGNING_ALLOWED_FOR_APPS)"
-		XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AppIcon
-	)
-
-	foreach(XCODE_RESOURCE LaunchScreen.storyboard Images.xcassets Settings.bundle vcmi_logo.png)
-		set(XCODE_RESOURCE_PATH ios/${XCODE_RESOURCE})
-		target_sources(vcmiclient PRIVATE ${XCODE_RESOURCE_PATH})
-		set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
-
-		# workaround to prevent CMAKE_SKIP_PRECOMPILE_HEADERS being added as compile flag
-		if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22.0" AND CMAKE_VERSION VERSION_LESS "3.25.0")
-			set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES LANGUAGE CXX)
-		endif()
-	endforeach()
-
-	set(CMAKE_EXE_LINKER_FLAGS "-Wl,-e,_client_main")
 endif()
 
-target_link_libraries(vcmiclient PRIVATE vcmiservercommon)
-if(ENABLE_SINGLE_APP_BUILD AND ENABLE_LAUNCHER)
-	target_link_libraries(vcmiclient PRIVATE vcmilauncher)
-endif()
+target_link_libraries(vcmiclientcommon PRIVATE vcmiservercommon)
 
-target_link_libraries(vcmiclient PRIVATE
+target_link_libraries(vcmiclientcommon PUBLIC
 		vcmi SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF
 )
 
 if(ffmpeg_LIBRARIES)
-	target_link_libraries(vcmiclient PRIVATE
+	target_link_libraries(vcmiclientcommon PRIVATE
 		${ffmpeg_LIBRARIES}
 	)
 else()
-	target_compile_definitions(vcmiclient PRIVATE DISABLE_VIDEO)
+	target_compile_definitions(vcmiclientcommon PRIVATE DISABLE_VIDEO)
 endif()
 
-target_include_directories(vcmiclient PUBLIC
+target_include_directories(vcmiclientcommon PUBLIC
 	${CMAKE_CURRENT_SOURCE_DIR}
 )
 
 if (ffmpeg_INCLUDE_DIRS)
-	target_include_directories(vcmiclient PRIVATE
+	target_include_directories(vcmiclientcommon PRIVATE
 		${ffmpeg_INCLUDE_DIRS}
 	)
 endif()
 
-vcmi_set_output_dir(vcmiclient "")
-enable_pch(vcmiclient)
-
-if(APPLE_IOS)
-	vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}")
-	add_custom_command(TARGET vcmiclient POST_BUILD
-		COMMAND ios/set_build_version.sh "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
-		COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --component "${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}" --config "$<CONFIG>" --prefix "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
-		COMMAND ios/rpath_remove_symlinks.sh
-		COMMAND ios/codesign.sh
-		WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
-	)
-	install(TARGETS vcmiclient DESTINATION Payload COMPONENT app) # for ipa generation with cpack
-elseif(ANDROID)
-	find_program(androidDeployQt androiddeployqt
-		PATHS "${qtBinDir}"
-	)
-	vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
-
-	add_custom_target(android_deploy ALL
-		COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --config "$<CONFIG>" --prefix "${androidQtBuildDir}"
-		COMMAND "${androidDeployQt}" --input "${CMAKE_BINARY_DIR}/androiddeployqt.json" --output "${androidQtBuildDir}" --android-platform "android-${ANDROID_TARGET_SDK_VERSION}" --verbose $<$<NOT:$<CONFIG:Debug>>:--release> ${ANDROIDDEPLOYQT_OPTIONS}
-		COMMAND_EXPAND_LISTS
-		VERBATIM
-		COMMENT "Create android package"
-	)
-	add_dependencies(android_deploy vcmiclient)
-else()
-	install(TARGETS vcmiclient DESTINATION ${BIN_DIR})
-endif()
-
-#install icons and desktop file on Linux
-if(NOT WIN32 AND NOT APPLE AND NOT ANDROID)
-	#FIXME: move to client makefile?
-	foreach(iconSize 16 22 32 48 64 128 256 512 1024 2048)
-		install(FILES "icons/vcmiclient.${iconSize}x${iconSize}.png"
-			DESTINATION "share/icons/hicolor/${iconSize}x${iconSize}/apps"
-			RENAME vcmiclient.png
-		)
-	endforeach()
-
-	install(FILES icons/vcmiclient.svg
-		DESTINATION share/icons/hicolor/scalable/apps
-		RENAME vcmiclient.svg
-	)
-	install(FILES icons/vcmiclient.desktop
-		DESTINATION share/applications
-	)
-endif()
+vcmi_set_output_dir(vcmiclientcommon "")
+enable_pch(vcmiclientcommon)

+ 1 - 11
client/CPlayerInterface.cpp

@@ -13,7 +13,6 @@
 #include <vcmi/Artifact.h>
 
 #include "CGameInfo.h"
-#include "CMT.h"
 #include "CServerHandler.h"
 #include "HeroMovementController.h"
 #include "PlayerLocalState.h"
@@ -1639,15 +1638,6 @@ void CPlayerInterface::showMarketWindow(const IMarket * market, const CGHeroInst
 		cb->selectionMade(0, queryID);
 	};
 
-	if (market->allowsTrade(EMarketMode::ARTIFACT_EXP) && market->getArtifactsStorage() == nullptr)
-	{
-		// compatibility check, safe to remove for 1.6
-		// 1.4 saves loaded in 1.5 will not be able to visit Altar of Sacrifice due to Altar now requiring different map object class
-		static_assert(ESerializationVersion::RELEASE_143 < ESerializationVersion::CURRENT, "Please remove this compatibility check once it no longer needed");
-		onWindowClosed();
-		return;
-	}
-
 	if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL)
 		GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP);
 	else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD)
@@ -1673,7 +1663,7 @@ void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroI
 	auto onWindowClosed = [this, queryID](){
 		cb->selectionMade(0, queryID);
 	};
-	GH.windows().createAndPushWindow<CUniversityWindow>(visitor, market, onWindowClosed);
+	GH.windows().createAndPushWindow<CUniversityWindow>(visitor, BuildingID::NONE, market, onWindowClosed);
 }
 
 void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor)

+ 10 - 2
client/CServerHandler.cpp

@@ -532,7 +532,10 @@ void CServerHandler::sendGuiAction(ui8 action) const
 
 void CServerHandler::sendRestartGame() const
 {
-	GH.windows().createAndPushWindow<CLoadingScreen>();
+	if(si->campState && !si->campState->getLoadingBackground().empty())
+		GH.windows().createAndPushWindow<CLoadingScreen>(si->campState->getLoadingBackground());
+	else
+		GH.windows().createAndPushWindow<CLoadingScreen>();
 	
 	LobbyRestartGame endGame;
 	sendLobbyPack(endGame);
@@ -576,7 +579,12 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const
 	verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool());
 
 	if(!settings["session"]["headless"].Bool())
-		GH.windows().createAndPushWindow<CLoadingScreen>();
+	{
+		if(si->campState && !si->campState->getLoadingBackground().empty())
+			GH.windows().createAndPushWindow<CLoadingScreen>(si->campState->getLoadingBackground());
+		else
+			GH.windows().createAndPushWindow<CLoadingScreen>();
+	}
 	
 	LobbyPrepareStartGame lpsg;
 	sendLobbyPack(lpsg);

+ 1 - 1
client/Client.h

@@ -207,7 +207,7 @@ public:
 	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {};
 
 	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
-	void changeFogOfWar(std::unordered_set<int3> & tiles, PlayerColor player, ETileVisibility mode) override {}
+	void changeFogOfWar(const std::unordered_set<int3> & tiles, PlayerColor player, ETileVisibility mode) override {}
 
 	void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override {};
 	void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {};

+ 10 - 1
client/ClientCommandManager.cpp

@@ -18,6 +18,7 @@
 #include "gui/CGuiHandler.h"
 #include "gui/WindowHandler.h"
 #include "render/IRenderHandler.h"
+#include "render/AssetGenerator.h"
 #include "ClientNetPackVisitors.h"
 #include "../lib/CConfigHandler.h"
 #include "../lib/gameState/CGameState.h"
@@ -38,7 +39,6 @@
 #include "../lib/CHeroHandler.h"
 #include "../lib/VCMIDirs.h"
 #include "../lib/logging/VisualLogger.h"
-#include "CMT.h"
 #include "../lib/serializer/Connection.h"
 
 #ifdef SCRIPTING_ENABLED
@@ -502,6 +502,12 @@ void ClientCommandManager::handleVsLog(std::istringstream & singleWordBuffer)
 	logVisual->setKey(key);
 }
 
+void ClientCommandManager::handleGenerateAssets()
+{
+	AssetGenerator::generateAll();
+	printCommandMessage("All assets generated");
+}
+
 void ClientCommandManager::printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType)
 {
 	switch(messageType)
@@ -624,6 +630,9 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
 	else if(commandName == "vslog")
 		handleVsLog(singleWordBuffer);
 
+	else if(message=="generate assets")
+		handleGenerateAssets();
+
 	else
 	{
 		if (!commandName.empty() && !vstd::iswithin(commandName[0], 0, ' ')) // filter-out debugger/IDE noise

+ 3 - 0
client/ClientCommandManager.h

@@ -84,6 +84,9 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
 	// shows object graph
 	void handleVsLog(std::istringstream & singleWordBuffer);
 
+	// generate all assets
+	void handleGenerateAssets();
+
 	// Prints in Chat the given message
 	void printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType = ELogLevel::NOT_SET);
 	void giveTurn(const PlayerColor &color);

+ 1 - 1
client/ClientNetPackVisitors.h

@@ -47,7 +47,7 @@ public:
 	void visitBulkRebalanceStacks(BulkRebalanceStacks & pack) override;
 	void visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) override;
 	void visitPutArtifact(PutArtifact & pack) override;
-	void visitEraseArtifact(EraseArtifact & pack) override;
+	void visitEraseArtifact(BulkEraseArtifacts & pack) override;
 	void visitBulkMoveArtifacts(BulkMoveArtifacts & pack) override;
 	void visitAssembledArtifact(AssembledArtifact & pack) override;
 	void visitDisassembledArtifact(DisassembledArtifact & pack) override;

+ 11 - 2
client/NetPacksClient.cpp

@@ -290,9 +290,10 @@ void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack)
 		callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::askToAssembleArtifact, pack.al);
 }
 
-void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack)
+void ApplyClientNetPackVisitor::visitEraseArtifact(BulkEraseArtifacts & pack)
 {
-	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactRemoved, pack.al);
+	for(const auto & slotErase : pack.posPack)
+		callInterfaceIfPresent(cl, cl.getOwner(pack.artHolder), &IGameEventsReceiver::artifactRemoved, ArtifactLocation(pack.artHolder, slotErase));
 }
 
 void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
@@ -361,6 +362,14 @@ void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
 void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack)
 {
 	cl.invalidatePaths();
+
+	if (pack.newWeekNotification)
+	{
+		const auto & newWeek = *pack.newWeekNotification;
+
+		std::string str = newWeek.text.toString();
+		callAllInterfaces(cl, &CGameInterface::showInfoDialog, newWeek.type, str, newWeek.components,(soundBase::soundID)newWeek.soundID);
+	}
 }
 
 void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack)

+ 5 - 1
client/NetPacksLobbyClient.cpp

@@ -35,6 +35,7 @@
 #include "../lib/CConfigHandler.h"
 #include "../lib/texts/CGeneralTextHandler.h"
 #include "../lib/serializer/Connection.h"
+#include "../lib/campaign/CampaignState.h"
 
 void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack)
 {
@@ -203,7 +204,10 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState &
 	if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN)
 	{
 		lobby->bonusSel = std::make_shared<CBonusSelection>();
-		GH.windows().pushWindow(lobby->bonusSel);
+		if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty())
+			GH.windows().createAndPushWindow<CampaignIntroVideo>(handler.si->campState->getIntroVideo(), handler.si->campState->getIntroVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getIntroVideoRim(), lobby->bonusSel);
+		else
+			GH.windows().pushWindow(lobby->bonusSel);
 	}
 
 	if(lobby->bonusSel)

+ 1 - 1
client/battle/BattleAnimationClasses.cpp

@@ -161,7 +161,7 @@ ECreatureAnimType AttackAnimation::findValidGroup( const std::vector<ECreatureAn
 const CCreature * AttackAnimation::getCreature() const
 {
 	if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS)
-		return owner.siegeController->getTurretCreature();
+		return owner.siegeController->getTurretCreature(attackingStack->initialPosition);
 	else
 		return attackingStack->unitType();
 }

+ 1 - 1
client/battle/BattleInterface.cpp

@@ -85,7 +85,7 @@ BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *
 	this->army2 = army2;
 
 	const CGTownInstance *town = getBattle()->battleGetDefendedTown();
-	if(town && town->hasFort())
+	if(town && town->fortificationsLevel().wallsHealth > 0)
 		siegeController.reset(new BattleSiegeController(*this, town));
 
 	windowObject = std::make_shared<BattleWindow>(*this);

+ 1 - 1
client/battle/BattleProjectileController.cpp

@@ -160,7 +160,7 @@ const CCreature & BattleProjectileController::getShooter(const CStack * stack) c
 	const CCreature * creature = stack->unitType();
 
 	if(creature->getId() == CreatureID::ARROW_TOWERS)
-		creature = owner.siegeController->getTurretCreature();
+		creature = owner.siegeController->getTurretCreature(stack->initialPosition);
 
 	if(creature->animation.missileFrameAngles.empty())
 	{

+ 37 - 30
client/battle/BattleSiegeController.cpp

@@ -27,6 +27,7 @@
 
 #include "../../CCallback.h"
 #include "../../lib/CStack.h"
+#include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 
@@ -34,30 +35,27 @@ ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual
 {
 	auto getImageIndex = [&]() -> int
 	{
-		bool isTower = (what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER);
+		int health = static_cast<int>(state);
 
-		switch (state)
+		switch (what)
 		{
-		case EWallState::REINFORCED :
-			return 1;
-		case EWallState::INTACT :
-			if (town->hasBuilt(BuildingID::CASTLE))
-				return 2; // reinforced walls were damaged
-			else
-				return 1;
-		case EWallState::DAMAGED :
-			// towers don't have separate image here - INTACT and DAMAGED is 1, DESTROYED is 2
-			if (isTower)
-				return 1;
-			else
-				return 2;
-		case EWallState::DESTROYED :
-			if (isTower)
-				return 2;
-			else
+			case EWallVisual::KEEP:
+			case EWallVisual::BOTTOM_TOWER:
+			case EWallVisual::UPPER_TOWER:
+				if (health > 0)
+					return 1;
+				else
+					return 2;
+			default:
+			{
+				int healthTotal = town->fortificationsLevel().wallsHealth;
+				if (healthTotal == health)
+					return 1;
+				if (health > 0)
+					return 2;
 				return 3;
-		}
-		return 1;
+			}
+		};
 	};
 
 	const std::string & prefix = town->town->clientInfo.siegePrefix;
@@ -128,16 +126,15 @@ ImagePath BattleSiegeController::getBattleBackgroundName() const
 
 bool BattleSiegeController::getWallPieceExistence(EWallVisual::EWallVisual what) const
 {
-	//FIXME: use this instead of buildings test?
-	//ui8 siegeLevel = owner.curInt->cb->battleGetSiegeLevel();
+	const auto & fortifications = town->fortificationsLevel();
 
 	switch (what)
 	{
-	case EWallVisual::MOAT:              return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid();
-	case EWallVisual::MOAT_BANK:         return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid();
-	case EWallVisual::KEEP_BATTLEMENT:   return town->hasBuilt(BuildingID::CITADEL) && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
-	case EWallVisual::UPPER_BATTLEMENT:  return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
-	case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
+	case EWallVisual::MOAT:              return fortifications.hasMoat && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid();
+	case EWallVisual::MOAT_BANK:         return fortifications.hasMoat && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid();
+	case EWallVisual::KEEP_BATTLEMENT:   return fortifications.citadelHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
+	case EWallVisual::UPPER_BATTLEMENT:  return fortifications.upperTowerHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
+	case EWallVisual::BOTTOM_BATTLEMENT: return fortifications.lowerTowerHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
 	default:                             return true;
 	}
 }
@@ -186,9 +183,19 @@ BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTo
 	}
 }
 
-const CCreature *BattleSiegeController::getTurretCreature() const
+const CCreature *BattleSiegeController::getTurretCreature(BattleHex position) const
 {
-	return town->town->clientInfo.siegeShooter.toCreature();
+	switch (position)
+	{
+		case BattleHex::CASTLE_CENTRAL_TOWER:
+			return town->fortificationsLevel().citadelShooter.toCreature();
+		case BattleHex::CASTLE_UPPER_TOWER:
+			return town->fortificationsLevel().upperTowerShooter.toCreature();
+		case BattleHex::CASTLE_BOTTOM_TOWER:
+			return town->fortificationsLevel().lowerTowerShooter.toCreature();
+	}
+
+	throw std::runtime_error("Unable to select shooter for tower at " + std::to_string(position.hex));
 }
 
 Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const

+ 1 - 1
client/battle/BattleSiegeController.h

@@ -104,7 +104,7 @@ public:
 	/// queries from other battle controllers
 	bool isAttackableByCatapult(BattleHex hex) const;
 	ImagePath getBattleBackgroundName() const;
-	const CCreature *getTurretCreature() const;
+	const CCreature *getTurretCreature(BattleHex turretPosition) const;
 	Point getTurretCreaturePosition( BattleHex position ) const;
 
 	const CGTownInstance *getSiegedTown() const;

+ 1 - 1
client/battle/BattleStacksController.cpp

@@ -191,7 +191,7 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 	{
 		assert(owner.siegeController);
 
-		const CCreature *turretCreature = owner.siegeController->getTurretCreature();
+		const CCreature *turretCreature = owner.siegeController->getTurretCreature(stack->initialPosition);
 
 		stackAnimation[stack->unitId()] = AnimationControls::getAnimation(turretCreature);
 		stackAnimation[stack->unitId()]->pos.h = turretCreatureAnimationHeight;

+ 6 - 1
client/eventsSDL/InputSourceMouse.cpp

@@ -23,6 +23,7 @@
 
 #include <SDL_events.h>
 #include <SDL_hints.h>
+#include <SDL_version.h>
 
 InputSourceMouse::InputSourceMouse()
 	:mouseToleranceDistance(settings["input"]["mouseToleranceDistance"].Integer())
@@ -69,7 +70,11 @@ void InputSourceMouse::handleEventMouseButtonDown(const SDL_MouseButtonEvent & b
 
 void InputSourceMouse::handleEventMouseWheel(const SDL_MouseWheelEvent & wheel)
 {
-	GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y) / GH.screenHandler().getScalingFactor(), GH.getCursorPosition());
+#if SDL_VERSION_ATLEAST(2,26,0)
+	GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y), Point(wheel.mouseX, wheel.mouseY) / GH.screenHandler().getScalingFactor());
+#else
+	GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y), GH.getCursorPosition());
+#endif
 }
 
 void InputSourceMouse::handleEventMouseButtonUp(const SDL_MouseButtonEvent & button)

+ 0 - 1
client/eventsSDL/InputSourceText.cpp

@@ -11,7 +11,6 @@
 #include "StdInc.h"
 #include "InputSourceText.h"
 
-#include "../CMT.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/EventDispatcher.h"
 #include "../render/IScreenHandler.h"

+ 0 - 1
client/eventsSDL/InputSourceTouch.cpp

@@ -14,7 +14,6 @@
 #include "InputHandler.h"
 
 #include "../../lib/CConfigHandler.h"
-#include "../CMT.h"
 #include "../CGameInfo.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"

+ 39 - 1
client/lobby/CBonusSelection.cpp

@@ -28,6 +28,7 @@
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/VideoWidget.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"
 #include "../render/IImage.h"
@@ -58,6 +59,41 @@
 #include "../../lib/mapObjects/CGHeroInstance.h"
 
 
+CampaignIntroVideo::CampaignIntroVideo(VideoPath video, ImagePath rim, std::shared_ptr<CBonusSelection> bonusSel)
+	: CWindowObject(BORDERED), bonusSel(bonusSel)
+{
+	OBJECT_CONSTRUCTION;
+
+	addUsedEvents(LCLICK | KEYBOARD);
+
+	pos = center(Rect(0, 0, 800, 600));
+
+	videoPlayer = std::make_shared<VideoWidgetOnce>(Point(80, 186), video, true, [this](){ exit(); });
+	setBackground(rim);
+
+	CCS->musich->stopMusic();
+}
+
+void CampaignIntroVideo::exit()
+{
+	close();
+	
+	if (!CSH->si->campState->getMusic().empty())
+		CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false);
+
+	GH.windows().pushWindow(bonusSel);
+}
+
+void CampaignIntroVideo::clickPressed(const Point & cursorPosition)
+{
+	exit();
+}
+
+void CampaignIntroVideo::keyPressed(EShortcut key)
+{
+	exit();
+}
+
 std::shared_ptr<CampaignState> CBonusSelection::getCampaign()
 {
 	return CSH->si->campState;
@@ -93,7 +129,9 @@ CBonusSelection::CBonusSelection()
 	labelCampaignDescription = std::make_shared<CLabel>(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]);
 	campaignDescription = std::make_shared<CTextBox>(getCampaign()->getDescriptionTranslated(), Rect(480, 86, 286, 117), 1);
 
-	mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated(), 285);
+	bool videoButtonActive = CSH->getState() == EClientState::GAMEPLAY;
+	int availableSpace = videoButtonActive ? 225 : 285;
+	mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated(), availableSpace );
 	labelMapDescription = std::make_shared<CLabel>(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]);
 	mapDescription = std::make_shared<CTextBox>("", Rect(480, 278, 292, 108), 1);
 

+ 17 - 0
client/lobby/CBonusSelection.h

@@ -12,6 +12,7 @@
 #include "../windows/CWindowObject.h"
 
 #include "../lib/campaign/CampaignConstants.h"
+#include "../lib/filesystem/ResourcePath.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -28,6 +29,22 @@ class CLabel;
 class CFlagBox;
 class ISelectionScreenInfo;
 class ExtraOptionsTab;
+class VideoWidgetOnce;
+class CBonusSelection;
+
+
+class CampaignIntroVideo : public CWindowObject
+{
+	std::shared_ptr<VideoWidgetOnce> videoPlayer;
+	std::shared_ptr<CBonusSelection> bonusSel;
+
+	void exit();
+public:
+	CampaignIntroVideo(VideoPath video, ImagePath rim, std::shared_ptr<CBonusSelection> bonusSel);
+
+	void clickPressed(const Point & cursorPosition) override;
+	void keyPressed(EShortcut key) override;
+};
 
 /// Campaign screen where you can choose one out of three starting bonuses
 class CBonusSelection : public CWindowObject

+ 3 - 0
client/lobby/OptionsTabBase.cpp

@@ -18,6 +18,7 @@
 #include "../widgets/TextControls.h"
 #include "../CServerHandler.h"
 #include "../CGameInfo.h"
+#include "../render/AssetGenerator.h"
 
 #include "../../lib/StartInfo.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
@@ -68,6 +69,8 @@ std::vector<SimturnsInfo> OptionsTabBase::getSimturnsPresets() const
 
 OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
 {
+	AssetGenerator::createAdventureOptionsCleanBackground();
+
 	recActions = 0;
 
 	auto setTimerPresetCallback = [this](int index){

+ 6 - 0
client/lobby/RandomMapTab.cpp

@@ -601,6 +601,12 @@ void RandomMapTab::loadOptions()
 		{
 			w->setItem(mapGenOptions->getMapTemplate());
 		}
+	} else
+	{
+		// Default settings
+		mapGenOptions->setRoadEnabled(RoadId(Road::DIRT_ROAD), true);
+		mapGenOptions->setRoadEnabled(RoadId(Road::GRAVEL_ROAD), true);
+		mapGenOptions->setRoadEnabled(RoadId(Road::COBBLESTONE_ROAD), true);
 	}
 	updateMapInfoByHost();
 

+ 2 - 0
client/lobby/SelectionTab.cpp

@@ -787,6 +787,8 @@ bool SelectionTab::isMapSupported(const CMapInfo & info)
 			return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["supported"].Bool();
 		case EMapFormat::SOD:
 			return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["supported"].Bool();
+		case EMapFormat::CHR:
+			return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_CHRONICLES)["supported"].Bool();
 		case EMapFormat::WOG:
 			return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["supported"].Bool();
 		case EMapFormat::HOTA:

+ 21 - 15
client/mainmenu/CMainMenu.cpp

@@ -80,6 +80,12 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode)
 
 	pos = background->center();
 
+	if(!config["video"].isNull())
+	{
+		Point videoPosition(config["video"]["x"].Integer(), config["video"]["y"].Integer());
+		videoPlayer = std::make_shared<VideoWidget>(videoPosition, VideoPath::fromJson(config["video"]["name"]), false);
+	}
+
 	for(const JsonNode & node : config["items"].Vector())
 		menuNameToEntry.push_back(node["name"].String());
 
@@ -90,12 +96,7 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode)
 	menuNameToEntry.push_back("credits");
 
 	tabs = std::make_shared<CTabbedInt>(std::bind(&CMenuScreen::createTab, this, _1));
-	if(!config["video"].isNull())
-	{
-		Point videoPosition(config["video"]["x"].Integer(), config["video"]["y"].Integer());
-		videoPlayer = std::make_shared<VideoWidget>(videoPosition, VideoPath::fromJson(config["video"]["name"]), false);
-	}
-	else
+	if(config["video"].isNull())
 		tabs->setRedrawParent(true);
 
 }
@@ -170,13 +171,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::newGame, true, {}, ELoadMode::NONE);};
+					return []() { CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE); };
 				case 1:
 					return []() { GH.windows().createAndPushWindow<CMultiMode>(ESelectionScreen::newGame); };
 				case 2:
-					return []() { CMainMenu::openLobby(ESelectionScreen::campaignList, true, {}, ELoadMode::NONE);};
+					return []() { CMainMenu::openLobby(ESelectionScreen::campaignList, true, {}, ELoadMode::NONE); };
 				case 3:
-					return std::bind(CMainMenu::startTutorial);
+					return []() { CMainMenu::startTutorial(); };
 				}
 				break;
 			}
@@ -185,25 +186,25 @@ 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); };
 				case 1:
 					return []() { GH.windows().createAndPushWindow<CMultiMode>(ESelectionScreen::loadGame); };
 				case 2:
-					return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::CAMPAIGN);};
+					return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::CAMPAIGN); };
 				case 3:
-					return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::TUTORIAL);};
+					return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::TUTORIAL); };
 
 				}
 			}
 			break;
 			case 4: //exit
 			{
-				return std::bind(CInfoWindow::showYesNoDialog, CGI->generaltexth->allTexts[69], std::vector<std::shared_ptr<CComponent>>(), do_quit, 0, PlayerColor(1));
+				return []() { CInfoWindow::showYesNoDialog(CGI->generaltexth->allTexts[69], std::vector<std::shared_ptr<CComponent>>(), do_quit, 0, PlayerColor(1)); };
 			}
 			break;
 			case 5: //highscores
 			{
-				return std::bind(CMainMenu::openHighScoreScreen);
+				return []() { CMainMenu::openHighScoreScreen(); };
 			}
 			}
 		}
@@ -628,7 +629,12 @@ void CSimpleJoinScreen::startConnection(const std::string & addr, ui16 port)
 }
 
 CLoadingScreen::CLoadingScreen()
-	: CWindowObject(BORDERED, getBackground())
+	: CLoadingScreen(getBackground())
+{
+}
+
+CLoadingScreen::CLoadingScreen(ImagePath background)
+	: CWindowObject(BORDERED, background)
 {
 	OBJECT_CONSTRUCTION;
 	

+ 1 - 0
client/mainmenu/CMainMenu.h

@@ -192,6 +192,7 @@ class CLoadingScreen : virtual public CWindowObject, virtual public Load::Progre
 
 public:	
 	CLoadingScreen();
+	CLoadingScreen(ImagePath background);
 	~CLoadingScreen();
 
 	void tick(uint32_t msPassed) override;

+ 1 - 0
client/mainmenu/CStatisticScreen.cpp

@@ -472,6 +472,7 @@ LineChart::LineChart(Rect position, std::string title, TData data, TIcons icons,
 	int gridLineCount = 10;
 	int gridStep = computeGridStep(maxVal, gridLineCount);
 	niceMaxVal = gridStep * std::ceil(maxVal / gridStep);
+	niceMaxVal = std::max(1, niceMaxVal); // avoid zero size Y axis (if all values are 0)
 
 	// calculate points in chart
 	auto getPoint = [this](int i, std::vector<float> data){

+ 116 - 0
client/render/AssetGenerator.cpp

@@ -0,0 +1,116 @@
+/*
+ * AssetGenerator.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 "AssetGenerator.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../render/IImage.h"
+#include "../render/IImageLoader.h"
+#include "../render/Canvas.h"
+#include "../render/IRenderHandler.h"
+
+#include "../lib/filesystem/Filesystem.h"
+
+void AssetGenerator::generateAll()
+{
+	createBigSpellBook();
+	createAdventureOptionsCleanBackground();
+}
+
+void AssetGenerator::createAdventureOptionsCleanBackground()
+{
+	std::string filename = "data/AdventureOptionsBackgroundClear.bmp";
+
+	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
+		return;
+
+	if(!CResourceHandler::get("local")->createResource(filename))
+		return;
+	ResourcePath savePath(filename, EResType::IMAGE);
+
+	auto res = ImagePath::builtin("ADVOPTBK");
+
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(res, EImageBlitMode::OPAQUE);
+
+	Canvas canvas = Canvas(Point(575, 585), CanvasScalingPolicy::IGNORE);
+	canvas.draw(img, Point(0, 0), Rect(0, 0, 575, 585));
+	canvas.draw(img, Point(54, 121), Rect(54, 123, 335, 1));
+	canvas.draw(img, Point(158, 84), Rect(156, 84, 2, 37));
+	canvas.draw(img, Point(234, 84), Rect(232, 84, 2, 37));
+	canvas.draw(img, Point(310, 84), Rect(308, 84, 2, 37));
+	canvas.draw(img, Point(53, 567), Rect(53, 520, 339, 3));
+	canvas.draw(img, Point(53, 520), Rect(53, 264, 339, 47));
+
+	std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
+
+	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+}
+
+void AssetGenerator::createBigSpellBook()
+{
+	std::string filename = "data/SpellBookLarge.bmp";
+
+	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
+		return;
+
+	if(!CResourceHandler::get("local")->createResource(filename))
+		return;
+	ResourcePath savePath(filename, EResType::IMAGE);
+
+	auto res = ImagePath::builtin("SpelBack");
+
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(res, EImageBlitMode::OPAQUE);
+	Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE);
+	// edges
+	canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45));
+	canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141));
+	canvas.draw(img, Point(705, 0), Rect(509, 38, 95, 45));
+	canvas.draw(img, Point(705, 460), Rect(509, 400, 95, 141));
+	// left / right
+	Canvas tmp1 = Canvas(Point(90, 355 - 45), CanvasScalingPolicy::IGNORE);
+	tmp1.draw(img, Point(0, 0), Rect(15, 38 + 45, 90, 355 - 45));
+	canvas.drawScaled(tmp1, Point(0, 45), Point(90, 415));
+	Canvas tmp2 = Canvas(Point(95, 355 - 45), CanvasScalingPolicy::IGNORE);
+	tmp2.draw(img, Point(0, 0), Rect(509, 38 + 45, 95, 355 - 45));
+	canvas.drawScaled(tmp2, Point(705, 45), Point(95, 415));
+	// top / bottom
+	Canvas tmp3 = Canvas(Point(409, 45), CanvasScalingPolicy::IGNORE);
+	tmp3.draw(img, Point(0, 0), Rect(100, 38, 409, 45));
+	canvas.drawScaled(tmp3, Point(90, 0), Point(615, 45));
+	Canvas tmp4 = Canvas(Point(409, 141), CanvasScalingPolicy::IGNORE);
+	tmp4.draw(img, Point(0, 0), Rect(100, 400, 409, 141));
+	canvas.drawScaled(tmp4, Point(90, 460), Point(615, 141));
+	// middle
+	Canvas tmp5 = Canvas(Point(409, 141), CanvasScalingPolicy::IGNORE);
+	tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38));
+	canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415));
+	// carpet
+	Canvas tmp6 = Canvas(Point(590, 59), CanvasScalingPolicy::IGNORE);
+	tmp6.draw(img, Point(0, 0), Rect(15, 484, 590, 59));
+	canvas.drawScaled(tmp6, Point(0, 545), Point(800, 59));
+	// remove bookmarks
+	for (int i = 0; i < 56; i++)
+		canvas.draw(Canvas(canvas, Rect(i < 30 ? 268 : 327, 464, 1, 46)), Point(269 + i, 464));
+	for (int i = 0; i < 56; i++)
+		canvas.draw(Canvas(canvas, Rect(469, 464, 1, 42)), Point(470 + i, 464));
+	for (int i = 0; i < 57; i++)
+		canvas.draw(Canvas(canvas, Rect(i < 30 ? 564 : 630, 464, 1, 44)), Point(565 + i, 464));
+	for (int i = 0; i < 56; i++)
+		canvas.draw(Canvas(canvas, Rect(656, 464, 1, 47)), Point(657 + i, 464));
+	// draw bookmarks
+	canvas.draw(img, Point(278, 464), Rect(220, 405, 37, 47));
+	canvas.draw(img, Point(481, 465), Rect(354, 406, 37, 41));
+	canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45));
+	canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47));
+
+	std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
+
+	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+}

+ 18 - 0
client/render/AssetGenerator.h

@@ -0,0 +1,18 @@
+/*
+ * AssetGenerator.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+class AssetGenerator
+{
+public:
+    static void generateAll();
+    static void createAdventureOptionsCleanBackground();
+    static void createBigSpellBook();
+};

+ 2 - 6
client/render/ImageLocator.cpp

@@ -56,12 +56,8 @@ bool ImageLocator::operator<(const ImageLocator & other) const
 		return scalingFactor < other.scalingFactor;
 	if(playerColored != other.playerColored)
 		return playerColored < other.playerColored;
-	if(layerShadow != other.layerShadow)
-		return layerShadow < other.layerShadow;
-	if(layerBody != other.layerBody)
-		return layerBody < other.layerBody;
-	if (layerOverlay != other.layerOverlay)
-		return layerOverlay < other.layerOverlay;
+	if(layer != other.layer)
+		return layer < other.layer;
 
 	return false;
 }

+ 12 - 4
client/render/ImageLocator.h

@@ -12,6 +12,15 @@
 #include "../../lib/filesystem/ResourcePath.h"
 #include "../../lib/constants/EntityIdentifiers.h"
 
+enum class EImageLayer
+{
+	ALL,
+
+	BODY,
+	SHADOW,
+	OVERLAY,
+};
+
 struct ImageLocator
 {
 	std::optional<ImagePath> image;
@@ -19,13 +28,12 @@ struct ImageLocator
 	int defFrame = -1;
 	int defGroup = -1;
 
+	PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE;
+
 	bool verticalFlip = false;
 	bool horizontalFlip = false;
 	int8_t scalingFactor = 1;
-	PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE;
-	bool layerShadow = false;
-	bool layerBody = true;
-	bool layerOverlay = false;
+	EImageLayer layer = EImageLayer::ALL;
 
 	ImageLocator() = default;
 	ImageLocator(const AnimationPath & path, int frame, int group);

+ 14 - 2
client/renderSDL/CBitmapFont.cpp

@@ -16,15 +16,17 @@
 #include "../render/Colors.h"
 #include "../render/IScreenHandler.h"
 
+#include "../../lib/CConfigHandler.h"
 #include "../../lib/Rect.h"
+#include "../../lib/VCMI_Lib.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/modding/CModHandler.h"
 #include "../../lib/texts/Languages.h"
 #include "../../lib/texts/TextOperations.h"
 #include "../../lib/vcmi_endian.h"
-#include "../../lib/VCMI_Lib.h"
 
 #include <SDL_surface.h>
+#include <SDL_image.h>
 
 struct AtlasLayout
 {
@@ -199,10 +201,20 @@ CBitmapFont::CBitmapFont(const std::string & filename):
 
 	if (GH.screenHandler().getScalingFactor() != 1)
 	{
-		auto scaledSurface = CSDL_Ext::scaleSurfaceIntegerFactor(atlasImage, GH.screenHandler().getScalingFactor());
+		static const std::map<std::string, EScalingAlgorithm> filterNameToEnum = {
+			{ "nearest", EScalingAlgorithm::NEAREST},
+			{ "bilinear", EScalingAlgorithm::BILINEAR},
+			{ "xbrz", EScalingAlgorithm::XBRZ}
+		};
+
+		auto filterName = settings["video"]["fontUpscalingFilter"].String();
+		EScalingAlgorithm algorithm = filterNameToEnum.at(filterName);
+		auto scaledSurface = CSDL_Ext::scaleSurfaceIntegerFactor(atlasImage, GH.screenHandler().getScalingFactor(), algorithm);
 		SDL_FreeSurface(atlasImage);
 		atlasImage = scaledSurface;
 	}
+
+	IMG_SavePNG(atlasImage, ("/home/ivan/font_" + filename).c_str());
 }
 
 CBitmapFont::~CBitmapFont()

+ 11 - 5
client/renderSDL/CTrueTypeFont.cpp

@@ -27,19 +27,25 @@ std::pair<std::unique_ptr<ui8[]>, ui64> CTrueTypeFont::loadData(const JsonNode &
 	return CResourceHandler::get()->load(ResourcePath(filename, EResType::TTF_FONT))->readAll();
 }
 
-TTF_Font * CTrueTypeFont::loadFont(const JsonNode &config)
+int CTrueTypeFont::getPointSize(const JsonNode & config) const
 {
-	int pointSizeBase = static_cast<int>(config["size"].Float());
 	int scalingFactor = getScalingFactor();
-	int pointSize = pointSizeBase * scalingFactor;
 
+	if (config.isNumber())
+		return config.Integer() * scalingFactor;
+	else
+		return config[scalingFactor-1].Integer();
+}
+
+TTF_Font * CTrueTypeFont::loadFont(const JsonNode &config)
+{
 	if(!TTF_WasInit() && TTF_Init()==-1)
 		throw std::runtime_error(std::string("Failed to initialize true type support: ") + TTF_GetError() + "\n");
 
-	return TTF_OpenFontRW(SDL_RWFromConstMem(data.first.get(), (int)data.second), 1, pointSize);
+	return TTF_OpenFontRW(SDL_RWFromConstMem(data.first.get(), data.second), 1, getPointSize(config["size"]));
 }
 
-int CTrueTypeFont::getFontStyle(const JsonNode &config)
+int CTrueTypeFont::getFontStyle(const JsonNode &config) const
 {
 	const JsonVector & names = config["style"].Vector();
 	int ret = 0;

+ 2 - 1
client/renderSDL/CTrueTypeFont.h

@@ -30,7 +30,8 @@ class CTrueTypeFont final : public IFont
 
 	std::pair<std::unique_ptr<ui8[]>, ui64> loadData(const JsonNode & config);
 	TTF_Font * loadFont(const JsonNode & config);
-	int getFontStyle(const JsonNode & config);
+	int getPointSize(const JsonNode & config) const;
+	int getFontStyle(const JsonNode & config) const;
 
 	void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override;
 public:

+ 8 - 10
client/renderSDL/ImageScaled.cpp

@@ -30,6 +30,8 @@ ImageScaled::ImageScaled(const ImageLocator & inputLocator, const std::shared_pt
 {
 	locator.scalingFactor = GH.screenHandler().getScalingFactor();
 	setBodyEnabled(true);
+	if (mode == EImageBlitMode::ALPHA)
+		setShadowEnabled(true);
 }
 
 std::shared_ptr<ISharedImage> ImageScaled::getSharedImage() const
@@ -45,7 +47,7 @@ void ImageScaled::scaleInteger(int factor)
 void ImageScaled::scaleTo(const Point & size)
 {
 	if (body)
-		body = body->scaleTo(size, nullptr); // FIXME: adjust for scaling
+		body = body->scaleTo(size * GH.screenHandler().getScalingFactor(), nullptr);
 }
 
 void ImageScaled::exportBitmap(const boost::filesystem::path &path) const
@@ -107,11 +109,10 @@ void ImageScaled::adjustPalette(const ColorFilter &shifter, uint32_t colorsToSki
 
 void ImageScaled::setShadowEnabled(bool on)
 {
+	assert(blitMode == EImageBlitMode::ALPHA);
 	if (on)
 	{
-		locator.layerBody = false;
-		locator.layerShadow = true;
-		locator.layerOverlay = false;
+		locator.layer = EImageLayer::SHADOW;
 		locator.playerColored = PlayerColor::CANNOT_DETERMINE;
 		shadow = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
 	}
@@ -123,9 +124,7 @@ void ImageScaled::setBodyEnabled(bool on)
 {
 	if (on)
 	{
-		locator.layerBody = true;
-		locator.layerShadow = false;
-		locator.layerOverlay = false;
+		locator.layer = blitMode == EImageBlitMode::ALPHA ? EImageLayer::BODY : EImageLayer::ALL;
 		locator.playerColored = playerColor;
 		body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
 	}
@@ -136,11 +135,10 @@ void ImageScaled::setBodyEnabled(bool on)
 
 void ImageScaled::setOverlayEnabled(bool on)
 {
+	assert(blitMode == EImageBlitMode::ALPHA);
 	if (on)
 	{
-		locator.layerBody = false;
-		locator.layerShadow = false;
-		locator.layerOverlay = true;
+		locator.layer = EImageLayer::OVERLAY;
 		locator.playerColored = PlayerColor::CANNOT_DETERMINE;
 		overlay = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
 	}

+ 5 - 7
client/renderSDL/RenderHandler.cpp

@@ -228,16 +228,14 @@ std::shared_ptr<ISharedImage> RenderHandler::scaleImage(const ImageLocator & loc
 	if (imageFiles.count(locator))
 		return imageFiles.at(locator);
 
-	auto handle = image->createImageReference(EImageBlitMode::OPAQUE);
+	auto handle = image->createImageReference(locator.layer == EImageLayer::ALL ? EImageBlitMode::OPAQUE : EImageBlitMode::ALPHA);
 
 	assert(locator.scalingFactor != 1); // should be filtered-out before
 
-
-
-	handle->setOverlayEnabled(locator.layerOverlay);
-	handle->setBodyEnabled(locator.layerBody);
-	handle->setShadowEnabled(locator.layerShadow);
-	if (locator.layerBody && locator.playerColored != PlayerColor::CANNOT_DETERMINE)
+	handle->setOverlayEnabled(locator.layer == EImageLayer::ALL || locator.layer == EImageLayer::OVERLAY);
+	handle->setBodyEnabled(locator.layer == EImageLayer::ALL || locator.layer == EImageLayer::BODY);
+	handle->setShadowEnabled(locator.layer == EImageLayer::ALL || locator.layer == EImageLayer::SHADOW);
+	if (locator.layer == EImageLayer::ALL && locator.playerColored != PlayerColor::CANNOT_DETERMINE)
 		handle->playerColored(locator.playerColored);
 
 	handle->scaleInteger(locator.scalingFactor);

+ 7 - 26
client/renderSDL/SDLImage.cpp

@@ -270,30 +270,10 @@ std::shared_ptr<ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palet
 	if (factor <= 0)
 		throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor));
 
-	if (palette && surf->format->palette)
+	if (palette && surf && surf->format->palette)
 		SDL_SetSurfacePalette(surf, palette);
 
-	/// Convert current surface to ARGB format suitable for xBRZ
-	/// TODO: skip its creation if this is format matches current surface (even if unlikely)
-	SDL_Surface * intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0);
-	SDL_Surface * scaled = CSDL_Ext::newSurface(Point(surf->w * factor, surf->h * factor), intermediate);
-
-	assert(intermediate->pitch == intermediate->w * 4);
-	assert(scaled->pitch == scaled->w * 4);
-
-	const uint32_t * srcPixels = static_cast<const uint32_t*>(intermediate->pixels);
-	uint32_t * dstPixels = static_cast<uint32_t*>(scaled->pixels);
-
-	// avoid excessive granulation - xBRZ prefers at least 8-16 lines per task
-	// TODO: compare performance and size of images, recheck values for potentially better parameters
-	const int granulation = std::clamp(surf->h / 64 * 8, 8, 64);
-
-	tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate](const tbb::blocked_range<size_t> & r)
-	{
-		xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, xbrz::ColorFormat::ARGB, {}, r.begin(), r.end());
-	});
-
-	SDL_FreeSurface(intermediate);
+	SDL_Surface * scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ);
 
 	auto ret = std::make_shared<SDLImageShared>(scaled);
 
@@ -307,7 +287,7 @@ std::shared_ptr<ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palet
 	// erase our own reference
 	SDL_FreeSurface(scaled);
 
-	if (surf->format->palette)
+	if (surf && surf->format->palette)
 		SDL_SetSurfacePalette(surf, originalPalette);
 
 	return ret;
@@ -472,13 +452,11 @@ void SDLImageIndexed::setShadowTransparency(float factor)
 	};
 
 	// seems to be used unconditionally
+	colorsSDL[0] = CSDL_Ext::toSDL(Colors::TRANSPARENCY);
 	colorsSDL[1] = CSDL_Ext::toSDL(shadow25);
 	colorsSDL[4] = CSDL_Ext::toSDL(shadow50);
 
 	// seems to be used only if color matches
-	if (colorsSimilar(originalPalette->colors[0], sourcePalette[0]))
-		colorsSDL[0] = CSDL_Ext::toSDL(Colors::TRANSPARENCY);
-
 	if (colorsSimilar(originalPalette->colors[2], sourcePalette[2]))
 		colorsSDL[2] = CSDL_Ext::toSDL(shadow25);
 
@@ -502,6 +480,9 @@ void SDLImageIndexed::setShadowEnabled(bool on)
 	if (on)
 		setShadowTransparency(1.0);
 
+	if (!on && blitMode == EImageBlitMode::ALPHA)
+		setShadowTransparency(0.0);
+
 	shadowEnabled = on;
 }
 

+ 20 - 7
client/renderSDL/SDL_Extensions.cpp

@@ -638,8 +638,8 @@ SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface * surf, int width, int height)
 	if(!surf || !width || !height)
 		return nullptr;
 
-	if (surf->w * 2 == width && surf->h * 2 == height)
-		return scaleSurfaceIntegerFactor(surf, 2);
+	// TODO: use xBRZ if possible? E.g. when scaling to 150% do 100% -> 200% via xBRZ and then linear downscale 200% -> 150%?
+	// Need to investigate which is optimal	for performance and for visuals
 
 	SDL_Surface * intermediate = SDL_ConvertSurface(surf, screen->format, 0);
 	SDL_Surface * ret = newSurface(Point(width, height), intermediate);
@@ -654,7 +654,7 @@ SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface * surf, int width, int height)
 	return ret;
 }
 
-SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor)
+SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor, EScalingAlgorithm algorithm)
 {
 	if(surf == nullptr || factor == 0)
 		return nullptr;
@@ -662,7 +662,7 @@ SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor
 	int newWidth = surf->w * factor;
 	int newHight = surf->h * factor;
 
-	SDL_Surface * intermediate = SDL_ConvertSurface(surf, screen->format, 0);
+	SDL_Surface * intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0);
 	SDL_Surface * ret = newSurface(Point(newWidth, newHight), intermediate);
 
 	assert(intermediate->pitch == intermediate->w * 4);
@@ -675,10 +675,23 @@ SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor
 	// TODO: compare performance and size of images, recheck values for potentially better parameters
 	const int granulation = std::clamp(surf->h / 64 * 8, 8, 64);
 
-	tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate](const tbb::blocked_range<size_t> & r)
+	switch (algorithm)
 	{
-		xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, xbrz::ColorFormat::ARGB, {}, r.begin(), r.end());
-	});
+		case EScalingAlgorithm::NEAREST:
+			xbrz::nearestNeighborScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
+			break;
+		case EScalingAlgorithm::BILINEAR:
+			xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
+			break;
+		case EScalingAlgorithm::XBRZ:
+			tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate](const tbb::blocked_range<size_t> & r)
+			{
+				xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, xbrz::ColorFormat::ARGB, {}, r.begin(), r.end());
+			});
+			break;
+		default:
+			throw std::runtime_error("invalid scaling algorithm!");
+	}
 
 	SDL_FreeSurface(intermediate);
 

+ 8 - 1
client/renderSDL/SDL_Extensions.h

@@ -27,6 +27,13 @@ class Point;
 
 VCMI_LIB_NAMESPACE_END
 
+enum class EScalingAlgorithm : int8_t
+{
+	NEAREST,
+	BILINEAR,
+	XBRZ
+};
+
 namespace CSDL_Ext
 {
 
@@ -92,7 +99,7 @@ using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &,
 
 	// bilinear filtering. Always returns rgba surface
 	SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height);
-	SDL_Surface * scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor);
+	SDL_Surface * scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor, EScalingAlgorithm scaler);
 
 	template<int bpp>
 	void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect);

+ 101 - 90
client/windows/CCastleInterface.cpp

@@ -145,7 +145,7 @@ void CBuildingRect::clickPressed(const Point & cursorPosition)
 	if(getBuilding() && area && (parent->selectedBuilding==this))
 	{
 		auto building = getBuilding();
-		parent->buildingClicked(building->bid, building->subId, building->upgrade);
+		parent->buildingClicked(building->bid);
 	}
 }
 
@@ -681,18 +681,76 @@ const CGHeroInstance * CCastleBuildings::getHero()
 		return town->garrisonHero;
 }
 
-void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades)
+void CCastleBuildings::buildingClicked(BuildingID building)
 {
-	logGlobal->trace("You've clicked on %d", (int)building.toEnum());
-	const CBuilding *b = town->town->buildings.find(building)->second;
+	BuildingID buildingToEnter = building;
+	for(;;)
+	{
+		const CBuilding *b = town->town->buildings.find(buildingToEnter)->second;
+
+		if (buildingTryActivateCustomUI(buildingToEnter, building))
+			return;
+
+		if (!b->upgrade.hasValue())
+		{
+			enterBuilding(building);
+			return;
+		}
+
+		buildingToEnter = b->upgrade;
+	}
+}
 
-	if (building >= BuildingID::DWELL_FIRST)
+bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, BuildingID buildingTarget)
+{
+	logGlobal->trace("You've clicked on %d", (int)buildingToTest.toEnum());
+	const CBuilding *b = town->town->buildings.at(buildingToTest);
+
+	if (town->getWarMachineInBuilding(buildingToTest).hasValue())
 	{
-		enterDwelling((BuildingID::getLevelFromDwelling(building)));
+		enterBlacksmith(buildingTarget, town->getWarMachineInBuilding(buildingToTest));
+		return true;
+	}
+
+	if (!b->marketModes.empty())
+	{
+		switch (*b->marketModes.begin())
+		{
+			case EMarketMode::CREATURE_UNDEAD:
+				GH.windows().createAndPushWindow<CTransformerWindow>(town, getHero(), nullptr);
+				return true;
+
+			case EMarketMode::RESOURCE_SKILL:
+				if (getHero())
+					GH.windows().createAndPushWindow<CUniversityWindow>(getHero(), buildingTarget, town, nullptr);
+				return true;
+
+			case EMarketMode::RESOURCE_RESOURCE:
+				// can't use allied marketplace
+				if (town->getOwner() == LOCPLINT->playerID)
+				{
+					GH.windows().createAndPushWindow<CMarketWindow>(town, getHero(), nullptr, *b->marketModes.begin());
+					return true;
+				}
+				else
+					return false;
+			default:
+				if(getHero())
+					GH.windows().createAndPushWindow<CMarketWindow>(town, getHero(), nullptr, *b->marketModes.begin());
+				else
+					LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
+				return true;
+		}
+	}
+
+	if (buildingToTest >= BuildingID::DWELL_FIRST)
+	{
+		enterDwelling((BuildingID::getLevelFromDwelling(buildingToTest)));
+		return true;
 	}
 	else
 	{
-		switch(building)
+		switch(buildingToTest)
 		{
 		case BuildingID::MAGES_GUILD_1:
 		case BuildingID::MAGES_GUILD_2:
@@ -700,139 +758,91 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil
 		case BuildingID::MAGES_GUILD_4:
 		case BuildingID::MAGES_GUILD_5:
 				enterMagesGuild();
-				break;
+				return true;
 
 		case BuildingID::TAVERN:
 				LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE);
-				break;
+				return true;
 
 		case BuildingID::SHIPYARD:
 				if(town->shipyardStatus() == IBoatGenerator::GOOD)
+				{
 					LOCPLINT->showShipyardDialog(town);
+					return true;
+				}
 				else if(town->shipyardStatus() == IBoatGenerator::BOAT_ALREADY_BUILT)
+				{
 					LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]);
-				break;
+					return true;
+				}
+				return false;
 
 		case BuildingID::FORT:
 		case BuildingID::CITADEL:
 		case BuildingID::CASTLE:
 				GH.windows().createAndPushWindow<CFortScreen>(town);
-				break;
+				return true;
 
 		case BuildingID::VILLAGE_HALL:
 		case BuildingID::CITY_HALL:
 		case BuildingID::TOWN_HALL:
 		case BuildingID::CAPITOL:
 				enterTownHall();
-				break;
-
-		case BuildingID::MARKETPLACE:
-				// can't use allied marketplace
-				if (town->getOwner() == LOCPLINT->playerID)
-					GH.windows().createAndPushWindow<CMarketWindow>(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE);
-				else
-					enterBuilding(building);
-				break;
-
-		case BuildingID::BLACKSMITH:
-				enterBlacksmith(town->town->warMachine);
-				break;
+				return true;
 
 		case BuildingID::SHIP:
 			LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); //Cannot build another boat
-			break;
+			return true;
 
 		case BuildingID::SPECIAL_1:
 		case BuildingID::SPECIAL_2:
 		case BuildingID::SPECIAL_3:
 		case BuildingID::SPECIAL_4:
-				switch (subID)
+				switch (b->subId)
 				{
-				case BuildingSubID::NONE:
-						enterBuilding(building);
-						break;
-
 				case BuildingSubID::MYSTIC_POND:
-						enterFountain(building, subID, upgrades);
-						break;
-
-				case BuildingSubID::ARTIFACT_MERCHANT:
-						if(town->visitingHero)
-							GH.windows().createAndPushWindow<CMarketWindow>(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_ARTIFACT);
-						else
-							LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
-						break;
-
-				case BuildingSubID::FOUNTAIN_OF_FORTUNE:
-						enterFountain(building, subID, upgrades);
-					break;
-
-				case BuildingSubID::FREELANCERS_GUILD:
-						if(getHero())
-							GH.windows().createAndPushWindow<CMarketWindow>(town, getHero(), nullptr, EMarketMode::CREATURE_RESOURCE);
-						else
-							LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
-						break;
-
-				case BuildingSubID::MAGIC_UNIVERSITY:
-						if (getHero())
-							GH.windows().createAndPushWindow<CUniversityWindow>(getHero(), town, nullptr);
-						else
-							enterBuilding(building);
-						break;
+						enterFountain(buildingToTest, b->subId, buildingTarget);
+						return true;
 
 				case BuildingSubID::CASTLE_GATE:
 						if (LOCPLINT->makingTurn)
+						{
 							enterCastleGate();
-						else
-							enterBuilding(building);
-						break;
-
-				case BuildingSubID::CREATURE_TRANSFORMER: //Skeleton Transformer
-						GH.windows().createAndPushWindow<CTransformerWindow>(town, getHero(), nullptr);
-						break;
+							return true;
+						}
+						return false;
 
 				case BuildingSubID::PORTAL_OF_SUMMONING:
 						if (town->creatures[town->town->creatures.size()].second.empty())//No creatures
 							LOCPLINT->showInfoDialog(CGI->generaltexth->tcommands[30]);
 						else
 							enterDwelling(town->town->creatures.size());
-						break;
-
-				case BuildingSubID::BALLISTA_YARD:
-						enterBlacksmith(ArtifactID::BALLISTA);
-						break;
-
-				case BuildingSubID::THIEVES_GUILD:
-						enterAnyThievesGuild();
-						break;
+						return true;
 
 				case BuildingSubID::BANK:
 						enterBank();
-						break;
-
-				default:
-					if(upgrades == BuildingID::TAVERN)
-						LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE);
-					else
-						enterBuilding(building);
-					break;
+						return true;
 				}
-				break;
+		}
+	}
 
-		default:
-				enterBuilding(building);
-				break;
+	for (auto const & bonus : b->buildingBonuses)
+	{
+		if (bonus->type == BonusType::THIEVES_GUILD_ACCESS)
+		{
+			enterAnyThievesGuild();
+			return true;
 		}
 	}
+	return false;
 }
 
-void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
+void CCastleBuildings::enterBlacksmith(BuildingID building, ArtifactID artifactID)
 {
 	const CGHeroInstance *hero = town->visitingHero;
 	if(!hero)
 	{
-		LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->town->buildings.find(BuildingID::BLACKSMITH)->second->getNameTranslated()));
+		LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->town->buildings.find(building)->second->getNameTranslated()));
 		return;
 	}
 	auto art = artifactID.toArtifact();
@@ -843,7 +853,7 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
 	{
 		for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO))
 		{
-			if(hero->getArt(slot) == nullptr)
+			if(hero->getArt(slot) == nullptr || hero->getArt(slot)->getTypeId() != artifactID)
 			{
 				possible = true;
 				break;
@@ -854,8 +864,9 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
 			}
 		}
 	}
-	CreatureID cre = art->getWarMachine();
-	GH.windows().createAndPushWindow<CBlacksmithDialog>(possible, cre, artifactID, hero->id);
+
+	CreatureID creatureID = artifactID.toArtifact()->getWarMachine();
+	GH.windows().createAndPushWindow<CBlacksmithDialog>(possible, creatureID, artifactID, hero->id);
 }
 
 void CCastleBuildings::enterBuilding(BuildingID building)

+ 3 - 2
client/windows/CCastleInterface.h

@@ -150,7 +150,7 @@ class CCastleBuildings : public CIntObject
 
 	const CGHeroInstance* getHero();//Select hero for buildings usage
 
-	void enterBlacksmith(ArtifactID artifactID);//support for blacksmith + ballista yard
+	void enterBlacksmith(BuildingID building, ArtifactID artifactID);//support for blacksmith + ballista yard
 	void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages
 	void enterCastleGate();
 	void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains
@@ -173,7 +173,8 @@ public:
 	void enterBank();
 	void enterToTheQuickRecruitmentWindow();
 
-	void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE);
+	bool buildingTryActivateCustomUI(BuildingID buildingToTest, BuildingID buildingTarget);
+	void buildingClicked(BuildingID building);
 	void addBuilding(BuildingID building);
 	void removeBuilding(BuildingID building);//FIXME: not tested!!!
 };

+ 2 - 4
client/windows/CKingdomInterface.cpp

@@ -585,9 +585,7 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
 			const CGMine * mine = dynamic_cast<const CGMine *>(object);
 			assert(mine);
 			minesCount[mine->producedResource]++;
-
-			if (mine->producedResource == EGameResID::GOLD)
-				totalIncome += mine->getProducedQuantity();
+			totalIncome += mine->dailyIncome()[EGameResID::GOLD];
 		}
 	}
 
@@ -596,7 +594,7 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
 	auto * playerSettings = LOCPLINT->cb->getPlayerSettings(LOCPLINT->playerID);
 	for(auto & hero : heroes)
 	{
-		totalIncome += hero->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(EGameResID::GOLD)))) * playerSettings->handicap.percentIncome / 100;
+		totalIncome += hero->dailyIncome()[EGameResID::GOLD];
 	}
 
 	//Add town income of all towns

+ 11 - 5
client/windows/CMessage.cpp

@@ -73,20 +73,19 @@ std::vector<std::string> CMessage::breakText(std::string text, size_t maxLineWid
 	// each iteration generates one output line
 	while(text.length())
 	{
-		ui32 lineWidth = 0; //in characters or given char metric
 		ui32 wordBreak = -1; //last position for line break (last space character)
 		ui32 currPos = 0; //current position in text
 		bool opened = false; //set to true when opening brace is found
 		std::string color; //color found
 
 		size_t symbolSize = 0; // width of character, in bytes
-		size_t glyphWidth = 0; // width of printable glyph, pixels
+
+		std::string printableString;
 
 		// loops till line is full or end of text reached
-		while(currPos < text.length() && text[currPos] != 0x0a && lineWidth < maxLineWidth)
+		while(currPos < text.length() && text[currPos] != 0x0a)
 		{
 			symbolSize = TextOperations::getUnicodeCharacterSize(text[currPos]);
-			glyphWidth = graphics->fonts[font]->getGlyphWidth(text.data() + currPos);
 
 			// candidate for line break
 			if(ui8(text[currPos]) <= ui8(' '))
@@ -116,7 +115,14 @@ std::vector<std::string> CMessage::breakText(std::string text, size_t maxLineWid
 				color = "";
 			}
 			else
-				lineWidth += glyphWidth;
+			{
+				std::string newPrintableString = printableString;
+				newPrintableString.append(text.data() + currPos, symbolSize);
+				if (graphics->fonts[font]->getStringWidth(newPrintableString) < maxLineWidth)
+					printableString.append(text.data() + currPos, symbolSize);
+				else
+					break;
+			}
 			currPos += symbolSize;
 		}
 

+ 3 - 54
client/windows/CSpellWindow.cpp

@@ -31,10 +31,7 @@
 #include "../widgets/TextControls.h"
 #include "../widgets/Buttons.h"
 #include "../adventureMap/AdventureMapInterface.h"
-#include "../render/IRenderHandler.h"
-#include "../render/IImage.h"
-#include "../render/IImageLoader.h"
-#include "../render/Canvas.h"
+#include "../render/AssetGenerator.h"
 
 #include "../../CCallback.h"
 
@@ -119,7 +116,8 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 
 	if(isBigSpellbook)
 	{
-		background = std::make_shared<CPicture>(createBigSpellBook(), Point(0, 0));
+		AssetGenerator::createBigSpellBook();
+		background = std::make_shared<CPicture>(ImagePath::builtin("SpellBookLarge"), 0, 0);
 		updateShadow();
 	}
 	else
@@ -221,55 +219,6 @@ CSpellWindow::~CSpellWindow()
 {
 }
 
-std::shared_ptr<IImage> CSpellWindow::createBigSpellBook()
-{
-	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(ImagePath::builtin("SpelBack"), EImageBlitMode::OPAQUE);
-	Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::AUTO);
-	// edges
-	canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45));
-	canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141));
-	canvas.draw(img, Point(705, 0), Rect(509, 38, 95, 45));
-	canvas.draw(img, Point(705, 460), Rect(509, 400, 95, 141));
-	// left / right
-	Canvas tmp1 = Canvas(Point(90, 355 - 45), CanvasScalingPolicy::AUTO);
-	tmp1.draw(img, Point(0, 0), Rect(15, 38 + 45, 90, 355 - 45));
-	canvas.drawScaled(tmp1, Point(0, 45), Point(90, 415));
-	Canvas tmp2 = Canvas(Point(95, 355 - 45), CanvasScalingPolicy::AUTO);
-	tmp2.draw(img, Point(0, 0), Rect(509, 38 + 45, 95, 355 - 45));
-	canvas.drawScaled(tmp2, Point(705, 45), Point(95, 415));
-	// top / bottom
-	Canvas tmp3 = Canvas(Point(409, 45), CanvasScalingPolicy::AUTO);
-	tmp3.draw(img, Point(0, 0), Rect(100, 38, 409, 45));
-	canvas.drawScaled(tmp3, Point(90, 0), Point(615, 45));
-	Canvas tmp4 = Canvas(Point(409, 141), CanvasScalingPolicy::AUTO);
-	tmp4.draw(img, Point(0, 0), Rect(100, 400, 409, 141));
-	canvas.drawScaled(tmp4, Point(90, 460), Point(615, 141));
-	// middle
-	Canvas tmp5 = Canvas(Point(409, 141), CanvasScalingPolicy::AUTO);
-	tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38));
-	canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415));
-	// carpet
-	Canvas tmp6 = Canvas(Point(590, 59), CanvasScalingPolicy::AUTO);
-	tmp6.draw(img, Point(0, 0), Rect(15, 484, 590, 59));
-	canvas.drawScaled(tmp6, Point(0, 545), Point(800, 59));
-	// remove bookmarks
-	for (int i = 0; i < 56; i++)
-		canvas.draw(Canvas(canvas, Rect(i < 30 ? 268 : 327, 464, 1, 46)), Point(269 + i, 464));
-	for (int i = 0; i < 56; i++)
-		canvas.draw(Canvas(canvas, Rect(469, 464, 1, 42)), Point(470 + i, 464));
-	for (int i = 0; i < 57; i++)
-		canvas.draw(Canvas(canvas, Rect(i < 30 ? 564 : 630, 464, 1, 44)), Point(565 + i, 464));
-	for (int i = 0; i < 56; i++)
-		canvas.draw(Canvas(canvas, Rect(656, 464, 1, 47)), Point(657 + i, 464));
-	// draw bookmarks
-	canvas.draw(img, Point(278, 464), Rect(220, 405, 37, 47));
-	canvas.draw(img, Point(481, 465), Rect(354, 406, 37, 41));
-	canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45));
-	canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47));
-
-	return GH.renderHandler().createImage(canvas.getInternalSurface());
-}
-
 void CSpellWindow::searchInput()
 {
 	if(searchBox)

+ 0 - 2
client/windows/CSpellWindow.h

@@ -113,8 +113,6 @@ class CSpellWindow : public CWindowObject
 	void turnPageLeft();
 	void turnPageRight();
 
-	std::shared_ptr<IImage> createBigSpellBook();
-
 	bool openOnBattleSpells;
 	std::function<void(SpellID)> onSpellSelect; //external processing of selected spell
 

+ 2 - 3
client/windows/GUIClasses.cpp

@@ -946,7 +946,7 @@ void CUniversityWindow::CItem::hover(bool on)
 		GH.statusbar()->clear();
 }
 
-CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function<void()> & onWindowClosed)
+CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, BuildingID building, const IMarket * _market, const std::function<void()> & onWindowClosed)
 	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("UNIVERS1")),
 	hero(_hero),
 	onWindowClosed(onWindowClosed),
@@ -961,8 +961,7 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket
 	if(auto town = dynamic_cast<const CGTownInstance *>(_market))
 	{
 		auto faction = town->town->faction->getId();
-		auto bid = town->town->getSpecialBuilding(BuildingSubID::MAGIC_UNIVERSITY)->bid;
-		titlePic = std::make_shared<CAnimImage>((*CGI->townh)[faction]->town->clientInfo.buildingsIcons, bid);
+		titlePic = std::make_shared<CAnimImage>((*CGI->townh)[faction]->town->clientInfo.buildingsIcons, building);
 	}
 	else if(auto uni = dynamic_cast<const CGUniversity *>(_market); uni->appearance)
 	{

+ 1 - 1
client/windows/GUIClasses.h

@@ -391,7 +391,7 @@ class CUniversityWindow final : public CStatusbarWindow, public IMarketHolder
 	std::function<void()> onWindowClosed;
 
 public:
-	CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function<void()> & onWindowClosed);
+	CUniversityWindow(const CGHeroInstance * _hero, BuildingID building, const IMarket * _market, const std::function<void()> & onWindowClosed);
 
 	void makeDeal(SecondarySkill skill);
 	void close() override;

+ 2 - 2
client/windows/InfoWindows.cpp

@@ -271,7 +271,7 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town)
 	: CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position))
 {
 	InfoAboutTown iah;
-	LOCPLINT->cb->getTownInfo(town, iah, LOCPLINT->localState->getCurrentTown()); //todo: should this be nearest hero?
+	LOCPLINT->cb->getTownInfo(town, iah, LOCPLINT->localState->getCurrentArmy()); //todo: should this be nearest hero?
 
 	OBJECT_CONSTRUCTION;
 	tooltip = std::make_shared<CTownTooltip>(Point(9, 10), iah);
@@ -281,7 +281,7 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero)
 	: CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("HEROQVBK"), toScreen(position))
 {
 	InfoAboutHero iah;
-	LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentHero()); //todo: should this be nearest hero?
+	LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentArmy()); //todo: should this be nearest hero?
 
 	OBJECT_CONSTRUCTION;
 	tooltip = std::make_shared<CHeroTooltip>(Point(9, 10), iah);

+ 1 - 1
client/CFocusableHelper.cpp → clientapp/CFocusableHelper.cpp

@@ -9,7 +9,7 @@
  */
 #include "StdInc.h"
 #include "CFocusableHelper.h"
-#include "widgets/CTextInput.h"
+#include "../client/widgets/CTextInput.h"
 
 void removeFocusFromActiveInput()
 {

+ 0 - 0
client/CFocusableHelper.h → clientapp/CFocusableHelper.h


+ 137 - 0
clientapp/CMakeLists.txt

@@ -0,0 +1,137 @@
+set(clientapp_SRCS
+		StdInc.cpp
+		EntryPoint.cpp
+)
+
+set(clientapp_HEADERS
+		StdInc.h
+)
+
+if(APPLE_IOS)
+	set(clientapp_SRCS ${clientapp_SRCS}
+		CFocusableHelper.cpp
+		ios/GameChatKeyboardHandler.m
+		ios/main.m
+		ios/startSDL.mm
+	)
+	set(clientapp_HEADERS ${clientapp_HEADERS}
+		CFocusableHelper.h
+		ios/GameChatKeyboardHandler.h
+		ios/startSDL.h
+	)
+endif()
+
+assign_source_group(${clientapp_SRCS} ${clientapp_HEADERS})
+
+if(ANDROID)
+	add_library(vcmiclient SHARED ${clientapp_SRCS} ${clientapp_HEADERS})
+	set_target_properties(vcmiclient PROPERTIES
+		OUTPUT_NAME "vcmiclient_${ANDROID_ABI}" # required by Qt
+	)
+else()
+	add_executable(vcmiclient ${clientapp_SRCS} ${clientapp_HEADERS})
+endif()
+
+target_link_libraries(vcmiclient PRIVATE vcmiclientcommon)
+
+if(ENABLE_SINGLE_APP_BUILD AND ENABLE_LAUNCHER)
+	target_link_libraries(vcmiclient PRIVATE vcmilauncher)
+endif()
+
+target_include_directories(vcmiclient
+	PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+if(WIN32)
+	target_sources(vcmiclient PRIVATE "VCMI_client.rc")
+	set_target_properties(vcmiclient
+		PROPERTIES
+			OUTPUT_NAME "VCMI_client"
+			PROJECT_LABEL "VCMI_client"
+	)
+	set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclient)
+	if(NOT ENABLE_DEBUG_CONSOLE)
+		set_target_properties(vcmiclient PROPERTIES WIN32_EXECUTABLE)
+		target_link_libraries(vcmiclient SDL2::SDL2main)
+	endif()
+	target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
+
+	# TODO: very hacky, find proper solution to copy AI dlls into bin dir
+	if(MSVC)
+		add_custom_command(TARGET vcmiclient POST_BUILD
+			WORKING_DIRECTORY "$<TARGET_FILE_DIR:vcmiclient>"
+			COMMAND ${CMAKE_COMMAND} -E copy AI/fuzzylite.dll fuzzylite.dll
+			COMMAND ${CMAKE_COMMAND} -E copy AI/tbb12.dll tbb12.dll
+		)
+	endif()
+elseif(APPLE_IOS)
+	set_target_properties(vcmiclient PROPERTIES
+		MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist"
+		XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
+		XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "$(CODE_SIGNING_ALLOWED_FOR_APPS)"
+		XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AppIcon
+	)
+
+	foreach(XCODE_RESOURCE LaunchScreen.storyboard Images.xcassets Settings.bundle vcmi_logo.png)
+		set(XCODE_RESOURCE_PATH ios/${XCODE_RESOURCE})
+		target_sources(vcmiclient PRIVATE ${XCODE_RESOURCE_PATH})
+		set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
+
+		# workaround to prevent CMAKE_SKIP_PRECOMPILE_HEADERS being added as compile flag
+		if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22.0" AND CMAKE_VERSION VERSION_LESS "3.25.0")
+			set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES LANGUAGE CXX)
+		endif()
+	endforeach()
+
+	set(CMAKE_EXE_LINKER_FLAGS "-Wl,-e,_client_main")
+endif()
+
+vcmi_set_output_dir(vcmiclient "")
+enable_pch(vcmiclient)
+
+if(APPLE_IOS)
+	vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}")
+	add_custom_command(TARGET vcmiclient POST_BUILD
+		COMMAND ios/set_build_version.sh "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
+		COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --component "${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}" --config "$<CONFIG>" --prefix "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
+		COMMAND ios/rpath_remove_symlinks.sh
+		COMMAND ios/codesign.sh
+		WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+	)
+	install(TARGETS vcmiclient DESTINATION Payload COMPONENT app) # for ipa generation with cpack
+elseif(ANDROID)
+	find_program(androidDeployQt androiddeployqt
+		PATHS "${qtBinDir}"
+	)
+	vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
+
+	add_custom_target(android_deploy ALL
+		COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --config "$<CONFIG>" --prefix "${androidQtBuildDir}"
+		COMMAND "${androidDeployQt}" --input "${CMAKE_BINARY_DIR}/androiddeployqt.json" --output "${androidQtBuildDir}" --android-platform "android-${ANDROID_TARGET_SDK_VERSION}" --verbose $<$<NOT:$<CONFIG:Debug>>:--release> ${ANDROIDDEPLOYQT_OPTIONS}
+		COMMAND_EXPAND_LISTS
+		VERBATIM
+		COMMENT "Create android package"
+	)
+	add_dependencies(android_deploy vcmiclient)
+else()
+	install(TARGETS vcmiclient DESTINATION ${BIN_DIR})
+endif()
+
+#install icons and desktop file on Linux
+if(NOT WIN32 AND NOT APPLE AND NOT ANDROID)
+	#FIXME: move to client makefile?
+	foreach(iconSize 16 22 32 48 64 128 256 512 1024 2048)
+		install(FILES "icons/vcmiclient.${iconSize}x${iconSize}.png"
+			DESTINATION "share/icons/hicolor/${iconSize}x${iconSize}/apps"
+			RENAME vcmiclient.png
+		)
+	endforeach()
+
+	install(FILES icons/vcmiclient.svg
+		DESTINATION share/icons/hicolor/scalable/apps
+		RENAME vcmiclient.svg
+	)
+	install(FILES icons/vcmiclient.desktop
+		DESTINATION share/applications
+	)
+endif()

+ 30 - 28
client/CMT.cpp → clientapp/EntryPoint.cpp

@@ -1,5 +1,5 @@
 /*
- * CMT.cpp, part of VCMI engine
+ * EntryPoint.cpp, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *
@@ -8,38 +8,38 @@
  *
  */
 
-// CMT.cpp : Defines the entry point for the console application.
+// EntryPoint.cpp : Defines the entry point for the console application.
+
 #include "StdInc.h"
-#include "CMT.h"
-
-#include "CGameInfo.h"
-#include "mainmenu/CMainMenu.h"
-#include "media/CEmptyVideoPlayer.h"
-#include "media/CMusicHandler.h"
-#include "media/CSoundHandler.h"
-#include "media/CVideoHandler.h"
-#include "gui/CursorHandler.h"
-#include "eventsSDL/InputHandler.h"
-#include "CPlayerInterface.h"
-#include "gui/CGuiHandler.h"
-#include "gui/WindowHandler.h"
-#include "CServerHandler.h"
-#include "ClientCommandManager.h"
-#include "windows/CMessage.h"
-#include "windows/InfoWindows.h"
-#include "render/IScreenHandler.h"
-#include "render/IRenderHandler.h"
-#include "render/Graphics.h"
-
-#include "../lib/CConfigHandler.h"
-#include "../lib/texts/CGeneralTextHandler.h"
+#include "../Global.h"
+
+#include "../client/CGameInfo.h"
+#include "../client/ClientCommandManager.h"
+#include "../client/CMT.h"
+#include "../client/CPlayerInterface.h"
+#include "../client/CServerHandler.h"
+#include "../client/eventsSDL/InputHandler.h"
+#include "../client/gui/CGuiHandler.h"
+#include "../client/gui/CursorHandler.h"
+#include "../client/gui/WindowHandler.h"
+#include "../client/mainmenu/CMainMenu.h"
+#include "../client/media/CEmptyVideoPlayer.h"
+#include "../client/media/CMusicHandler.h"
+#include "../client/media/CSoundHandler.h"
+#include "../client/media/CVideoHandler.h"
+#include "../client/render/Graphics.h"
+#include "../client/render/IRenderHandler.h"
+#include "../client/render/IScreenHandler.h"
+#include "../client/windows/CMessage.h"
+#include "../client/windows/InfoWindows.h"
+
 #include "../lib/CThreadHelper.h"
 #include "../lib/ExceptionsCommon.h"
-#include "../lib/VCMIDirs.h"
-#include "../lib/VCMI_Lib.h"
 #include "../lib/filesystem/Filesystem.h"
-
 #include "../lib/logging/CBasicLogConfigurator.h"
+#include "../lib/texts/CGeneralTextHandler.h"
+#include "../lib/VCMI_Lib.h"
+#include "../lib/VCMIDirs.h"
 
 #include <boost/program_options.hpp>
 #include <vstd/StringUtils.h>
@@ -525,6 +525,8 @@ void handleQuit(bool ask)
 		CInfoWindow::showYesNoDialog(CGI->generaltexth->allTexts[69], {}, quitApplication, {}, PlayerColor(1));
 }
 
+/// Notify user about encountered fatal error and terminate the game
+/// TODO: decide on better location for this method
 void handleFatalError(const std::string & message, bool terminate)
 {
 	logGlobal->error("FATAL ERROR ENCOUNTERED, VCMI WILL NOW TERMINATE");

+ 11 - 0
clientapp/StdInc.cpp

@@ -0,0 +1,11 @@
+/*
+ * StdInc.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
+ *
+ */
+// Creates the precompiled header
+#include "StdInc.h"

+ 14 - 0
clientapp/StdInc.h

@@ -0,0 +1,14 @@
+/*
+ * StdInc.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../Global.h"
+
+VCMI_LIB_USING_NAMESPACE

+ 0 - 0
client/VCMI_client.rc → clientapp/VCMI_client.rc


+ 0 - 0
client/ios/GameChatKeyboardHandler.h → clientapp/ios/GameChatKeyboardHandler.h


+ 0 - 0
client/ios/GameChatKeyboardHandler.m → clientapp/ios/GameChatKeyboardHandler.m


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/Contents.json → clientapp/ios/Images.xcassets/AppIcon.appiconset/Contents.json


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/AppIcon.appiconset/[email protected] → clientapp/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 0 - 0
client/ios/Images.xcassets/Contents.json → clientapp/ios/Images.xcassets/Contents.json


+ 0 - 0
client/ios/Info.plist → clientapp/ios/Info.plist


+ 0 - 0
client/ios/LaunchScreen.storyboard → clientapp/ios/LaunchScreen.storyboard


+ 0 - 0
client/ios/Settings.bundle/Root.plist → clientapp/ios/Settings.bundle/Root.plist


+ 0 - 0
client/ios/Settings.bundle/en.lproj/Root.strings → clientapp/ios/Settings.bundle/en.lproj/Root.strings


+ 0 - 0
client/ios/Settings.bundle/ru.lproj/Root.strings → clientapp/ios/Settings.bundle/ru.lproj/Root.strings


+ 0 - 0
client/ios/main.m → clientapp/ios/main.m


+ 0 - 0
client/ios/startSDL.h → clientapp/ios/startSDL.h


+ 0 - 0
client/ios/startSDL.mm → clientapp/ios/startSDL.mm


+ 0 - 0
client/ios/vcmi_logo.png → clientapp/ios/vcmi_logo.png


+ 9 - 0
config/bonuses.json

@@ -591,6 +591,15 @@
 		{
 			"icon":  "zvs/Lib1.res/MEGABREATH"
 		}
+	},
+
+	"DISINTEGRATE":
+	{
+		"graphics":
+		{
+			"icon":  "zvs/Lib1.res/DISINTEGRATE"
+		}
+
 	}
 }
 

+ 68 - 4
config/buildingsLibrary.json

@@ -10,13 +10,40 @@
 			{
 				"type": "MORALE",
 				"val": 1
+			},
+			{
+				"propagator": "PLAYER_PROPAGATOR",
+				"type": "THIEVES_GUILD_ACCESS",
+				"val": 1
 			}
 		]
 	},
 	"shipyard":       { "id" : 6 },
-	"fort":           { "id" : 7 },
-	"citadel":        { "id" : 8,  "upgrades" : "fort" },
-	"castle":         { "id" : 9,  "upgrades" : "citadel" },
+	"fort": {
+		"id" : 7,
+		"fortifications" : {
+			"wallsHealth" : 2
+		}
+	},
+	
+	"citadel": {
+		"id" : 8,
+		"upgrades" : "fort",
+		"fortifications" : {
+			"citadelHealth" : 2,
+			"hasMoat" : true
+		}
+	},
+	
+	"castle": {
+		"id" : 9,
+		"upgrades" : "citadel",
+		"fortifications" : {
+			"wallsHealth" : 3,
+			"upperTowerHealth" : 2,
+			"lowerTowerHealth" : 2
+		}
+	},
 	
 	"villageHall": {
 		"id" : 10,
@@ -43,7 +70,10 @@
 		"produce": { "gold": 4000 }
 	},
 
-	"marketplace":    { "id" : 14 },
+	"marketplace":    { 
+		"id" : 14,
+		"marketModes" : ["resource-resource", "resource-player"]
+	},
 	"resourceSilo":   { "id" : 15, "requires" : [ "marketplace" ] },
 	"blacksmith":     { "id" : 16 },
 
@@ -198,5 +228,39 @@
 				}
 			]
 		}
+	},
+	
+	// Section 3 - markets
+	"artifactMerchant" : {
+		"requires" : [ "marketplace" ],
+		"marketModes" : ["resource-artifact", "artifact-resource"]
+	},
+	
+	"freelancersGuild" : {
+		"requires" : [ "marketplace" ],
+		"marketModes" : ["creature-resource"]
+	},
+	
+	"magicUniversity" : {
+		"marketModes" : ["resource-skill"]
+	},
+	
+	"creatureTransformer" : {
+		"marketModes" : ["creature-undead"]
+	},
+	
+	// Section 4 - buildings that now have dedicated mechanics
+	"ballistaYard": {
+		"blacksmith" : "ballista"
+	},
+	
+	"thievesGuild" : {
+		"bonuses": [
+			{
+				"propagator": "PLAYER_PROPAGATOR",
+				"type": "THIEVES_GUILD_ACCESS",
+				"val": 2
+			}
+		]
 	}
 }

+ 254 - 0
config/campaignOverrides.json

@@ -0,0 +1,254 @@
+{
+	"MAPS/HC1_MAIN" : { // Heroes Chronicles 1
+		"regions":
+		{
+			"background": "chronicles_1/CamBkHc",
+			"prefix": "chronicles_1/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_1/ABVOFL4" },
+			{ "voiceProlog": "chronicles_1/H3X2UAE" },
+			{ "voiceProlog": "chronicles_1/H3X2BBA" },
+			{ "voiceProlog": "chronicles_1/H3X2RND" },
+			{ "voiceProlog": "chronicles_1/G1C" },
+			{ "voiceProlog": "chronicles_1/G2C" },
+			{ "voiceProlog": "chronicles_1/ABVOFL3" },
+			{ "voiceProlog": "chronicles_1/H3X2BBF", "voiceEpilog": "chronicles_1/N1C_D" }
+		],
+		"loadingBackground": "chronicles_1/LoadBar",
+		"introVideoRim": "chronicles_1/INTRORIM",
+		"introVideo": "chronicles_1/Intro"
+	},
+	"MAPS/HC2_MAIN" : { // Heroes Chronicles 2
+		"regions":
+		{
+			"background": "chronicles_2/CamBkHc",
+			"prefix": "chronicles_2/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_2/H3X2ELB" },
+			{ "voiceProlog": "chronicles_2/H3X2NBA" },
+			{ "voiceProlog": "chronicles_2/H3X2RNA" },
+			{ "voiceProlog": "chronicles_2/ABVOAB8" },
+			{ "voiceProlog": "chronicles_2/H3X2UAL" },
+			{ "voiceProlog": "chronicles_2/E1A" },
+			{ "voiceProlog": "chronicles_2/ABVOAB2" },
+			{ "voiceProlog": "chronicles_2/G1A", "voiceEpilog": "chronicles_2/S1C" }
+		],
+		"loadingBackground": "chronicles_2/LoadBar",
+		"introVideoRim": "chronicles_2/INTRORIM",
+		"introVideo": "chronicles_2/Intro"
+	},
+	"MAPS/HC3_MAIN" : { // Heroes Chronicles 3
+		"regions":
+		{
+			"background": "chronicles_3/CamBkHc",
+			"prefix": "chronicles_3/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_3/G2C" },
+			{ "voiceProlog": "chronicles_3/ABVOAB1" },
+			{ "voiceProlog": "chronicles_3/G2D" },
+			{ "voiceProlog": "chronicles_3/E1B" },
+			{ "voiceProlog": "chronicles_3/ABVOAB2" },
+			{ "voiceProlog": "chronicles_3/ABVOAB4" },
+			{ "voiceProlog": "chronicles_3/ABVOAB6" },
+			{ "voiceProlog": "chronicles_3/G3B", "voiceEpilog": "chronicles_3/ABVOFL2" }
+		],
+		"loadingBackground": "chronicles_3/LoadBar",
+		"introVideoRim": "chronicles_3/INTRORIM",
+		"introVideo": "chronicles_3/Intro"
+	},
+	"MAPS/HC4_MAIN" : { // Heroes Chronicles 4
+		"regions":
+		{
+			"background": "chronicles_4/CamBkHc",
+			"prefix": "chronicles_4/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_4/ABVOAB1" },
+			{ "voiceProlog": "chronicles_4/ABVODB4" },
+			{ "voiceProlog": "chronicles_4/H3X2ELC" },
+			{ "voiceProlog": "chronicles_4/ABVODS2" },
+			{ "voiceProlog": "chronicles_4/ABVODS1" },
+			{ "voiceProlog": "chronicles_4/ABVODS3" },
+			{ "voiceProlog": "chronicles_4/ABVODS4" },
+			{ "voiceProlog": "chronicles_4/H3X2NBD", "voiceEpilog": "chronicles_4/S1C" }
+		],
+		"loadingBackground": "chronicles_4/LoadBar",
+		"introVideoRim": "chronicles_4/INTRORIM",
+		"introVideo": "chronicles_4/Intro"
+	},
+	"MAPS/HC5_MAIN" : { // Heroes Chronicles 5
+		"regions":
+		{
+			"background": "chronicles_5/CamBkHc",
+			"prefix": "chronicles_5/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 34, "y": 184 },
+				{ "infix": "2", "x": 235, "y": 184 },
+				{ "infix": "3", "x": 34, "y": 320 },
+				{ "infix": "4", "x": 235, "y": 320 },
+				{ "infix": "5", "x": 129, "y": 459 }
+			]
+		},
+		"scenarioCount": 5,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_5/ABVOAB1" },
+			{ "voiceProlog": "chronicles_5/H3X2RNA" },
+			{ "voiceProlog": "chronicles_5/ABVOFL2" },
+			{ "voiceProlog": "chronicles_5/ABVOFL4" },
+			{ "voiceProlog": "chronicles_5/H3X2UAH", "voiceEpilog": "chronicles_5/N1C_D"  }
+		],
+		"loadingBackground": "chronicles_5/LoadBar",
+		"introVideoRim": "chronicles_5/INTRORIM",
+		"introVideo": "chronicles_5/Intro"
+	},
+	"MAPS/HC6_MAIN" : { // Heroes Chronicles 6
+		"regions":
+		{
+			"background": "chronicles_6/CamBkHc",
+			"prefix": "chronicles_6/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 34, "y": 184 },
+				{ "infix": "2", "x": 235, "y": 184 },
+				{ "infix": "3", "x": 34, "y": 320 },
+				{ "infix": "4", "x": 235, "y": 320 },
+				{ "infix": "5", "x": 129, "y": 459 }
+			]
+		},
+		"scenarioCount": 5,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_6/H3X2ELB" },
+			{ "voiceProlog": "chronicles_6/E1A" },
+			{ "voiceProlog": "chronicles_6/H3X2BBA" },
+			{ "voiceProlog": "chronicles_6/ABVOAB2" },
+			{ "voiceProlog": "chronicles_6/ABVOAB5", "voiceEpilog": "chronicles_6/ABVODB2"  }
+		],
+		"loadingBackground": "chronicles_6/LoadBar",
+		"introVideoRim": "chronicles_6/INTRORIM",
+		"introVideo": "chronicles_6/Intro"
+	},
+	"MAPS/HC7_MAIN" : { // Heroes Chronicles 7
+		"regions":
+		{
+			"background": "chronicles_7/CamBkHc",
+			"prefix": "chronicles_7/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_7/ABVOFL2" },
+			{ "voiceProlog": "chronicles_7/ABVOFL3" },
+			{ "voiceProlog": "chronicles_7/N1C_D" },
+			{ "voiceProlog": "chronicles_7/S1C" },
+			{ "voiceProlog": "chronicles_7/H3X2UAB" },
+			{ "voiceProlog": "chronicles_7/E2C" },
+			{ "voiceProlog": "chronicles_7/H3X2NBE" },
+			{ "voiceProlog": "chronicles_7/ABVOFW4", "voiceEpilog": "chronicles_7/ABVOAB1" }
+		],
+		"loadingBackground": "chronicles_7/LoadBar",
+		"introVideoRim": "chronicles_7/INTRORIM",
+		"introVideo": "chronicles_7/Intro5"
+	},
+	"MAPS/HC8_MAIN" : { // Heroes Chronicles 8
+		"regions":
+		{
+			"background": "chronicles_8/CamBkHc",
+			"prefix": "chronicles_8/HcSc",
+			"suffix": ["1", "2", "3"],
+			"color_suffix_length": 0,
+			"desc": [
+				{ "infix": "1", "x": 27, "y": 43 },
+				{ "infix": "2", "x": 231, "y": 43 },
+				{ "infix": "3", "x": 27, "y": 178 },
+				{ "infix": "4", "x": 231, "y": 178 },
+				{ "infix": "5", "x": 27, "y": 312 },
+				{ "infix": "6", "x": 231, "y": 312 },
+				{ "infix": "7", "x": 27, "y": 447 },
+				{ "infix": "8", "x": 231, "y": 447 }
+			]
+		},
+		"scenarioCount": 8,
+		"scenarios": [
+			{ "voiceProlog": "chronicles_8/H3X2RNB" },
+			{ "voiceProlog": "chronicles_8/ABVOAB9" },
+			{ "voiceProlog": "chronicles_8/H3X2BBB" },
+			{ "voiceProlog": "chronicles_8/ABVODS1" },
+			{ "voiceProlog": "chronicles_8/H3X2ELA" },
+			{ "voiceProlog": "chronicles_8/E1B" },
+			{ "voiceProlog": "chronicles_8/H3X2BBD" },
+			{ "voiceProlog": "chronicles_8/H3X2ELE", "voiceEpilog": "chronicles_8/ABVOAB7" }
+		],
+		"loadingBackground": "chronicles_8/LoadBar",
+		"introVideoRim": "chronicles_8/INTRORIM",
+		"introVideo": "chronicles_8/Intro6"
+	}
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است