Selaa lähdekoodia

Merge pull request #4935 from IvanSavenko/misc_fixes2

Miscellaneous fixes for reported issues
Ivan Savenko 11 kuukautta sitten
vanhempi
sitoutus
f0a71c9e21
45 muutettua tiedostoa jossa 274 lisäystä ja 257 poistoa
  1. 1 1
      AI/Nullkiller/AIGateway.cpp
  2. 1 1
      AI/VCAI/VCAI.cpp
  3. 1 1
      client/ClientCommandManager.cpp
  4. 10 8
      client/battle/BattleAnimationClasses.cpp
  5. 4 3
      client/battle/BattleAnimationClasses.h
  6. 5 4
      client/battle/BattleEffectsController.cpp
  7. 3 2
      client/battle/BattleEffectsController.h
  8. 1 3
      client/battle/BattleFieldController.cpp
  9. 2 2
      client/battle/BattleInterface.cpp
  10. 1 1
      client/battle/BattleInterfaceClasses.cpp
  11. 4 4
      client/battle/BattleObstacleController.cpp
  12. 1 1
      client/battle/BattleStacksController.cpp
  13. 5 5
      client/battle/CreatureAnimation.cpp
  14. 15 22
      client/mapView/MapRenderer.cpp
  15. 1 1
      client/mapView/MapRenderer.h
  16. 23 6
      client/render/IImage.h
  17. 6 2
      client/render/ImageLocator.cpp
  18. 4 11
      client/render/ImageLocator.h
  19. 52 33
      client/renderSDL/ImageScaled.cpp
  20. 1 3
      client/renderSDL/ImageScaled.h
  21. 5 13
      client/renderSDL/RenderHandler.cpp
  22. 46 51
      client/renderSDL/SDLImage.cpp
  23. 1 12
      client/renderSDL/SDLImage.h
  24. 1 1
      client/renderSDL/SDL_Extensions.cpp
  25. 3 3
      client/widgets/CGarrisonInt.cpp
  26. 5 5
      client/widgets/Images.cpp
  27. 1 1
      client/windows/CCastleInterface.cpp
  28. 2 1
      client/windows/CHeroWindow.cpp
  29. 1 1
      client/windows/GUIClasses.cpp
  30. 2 1
      config/schemas/spell.json
  31. 1 1
      config/spells/ability.json
  32. 2 2
      config/spells/offensive.json
  33. 1 1
      config/spells/other.json
  34. 2 2
      config/spells/timed.json
  35. 1 1
      docs/modders/Entities_Format/Spell_Format.md
  36. 10 5
      docs/modders/HD_Graphics.md
  37. 2 2
      lib/CGameInfoCallback.cpp
  38. 12 8
      lib/StartInfo.cpp
  39. 2 2
      lib/StartInfo.h
  40. 8 17
      lib/entities/hero/CHeroClass.cpp
  41. 7 4
      lib/mapObjectConstructors/DwellingInstanceConstructor.cpp
  42. 2 1
      lib/mapObjects/CGHeroInstance.cpp
  43. 8 2
      lib/spells/CSpellHandler.cpp
  44. 1 0
      lib/spells/CSpellHandler.h
  45. 7 6
      server/CGameHandler.cpp

+ 1 - 1
AI/Nullkiller/AIGateway.cpp

@@ -763,7 +763,7 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan
 	//you can't request action from action-response thread
 	requestActionASAP([=]()
 	{
-		if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isSteadwickFallCampaignMission())
+		if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isRestorationOfErathiaCampaign())
 		{
 			pickBestCreatures(down, up);
 		}

+ 1 - 1
AI/VCAI/VCAI.cpp

@@ -731,7 +731,7 @@ void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance *
 	//you can't request action from action-response thread
 	requestActionASAP([=]()
 	{
-		if(removableUnits && !cb->getStartInfo()->isSteadwickFallCampaignMission())
+		if(removableUnits && !cb->getStartInfo()->isRestorationOfErathiaCampaign())
 			pickBestCreatures(down, up);
 
 		answerQuery(queryID, 0);

+ 1 - 1
client/ClientCommandManager.cpp

@@ -394,7 +394,7 @@ void ClientCommandManager::handleDef2bmpCommand(std::istringstream& singleWordBu
 {
 	std::string URI;
 	singleWordBuffer >> URI;
-	auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin(URI), EImageBlitMode::ALPHA);
+	auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin(URI), EImageBlitMode::SIMPLE);
 	anim->exportBitmaps(VCMIDirs::get().userExtractedPath());
 }
 

+ 10 - 8
client/battle/BattleAnimationClasses.cpp

@@ -881,9 +881,10 @@ uint32_t CastAnimation::getAttackClimaxFrame() const
 	return maxFrames / 2;
 }
 
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, bool reversed):
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, float transparencyFactor, bool reversed):
 	BattleAnimation(owner),
-	animation(GH.renderHandler().loadAnimation(animationName, EImageBlitMode::ALPHA)),
+	animation(GH.renderHandler().loadAnimation(animationName, EImageBlitMode::SIMPLE)),
+	transparencyFactor(transparencyFactor),
 	effectFlags(effects),
 	effectFinished(false),
 	reversed(reversed)
@@ -892,32 +893,32 @@ EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath &
 }
 
 EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<BattleHex> hex, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
+	EffectAnimation(owner, animationName, effects, 1.0f, reversed)
 {
 	battlehexes = hex;
 }
 
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, float transparencyFactor, bool reversed):
+	EffectAnimation(owner, animationName, effects, transparencyFactor, reversed)
 {
 	assert(hex.isValid());
 	battlehexes.push_back(hex);
 }
 
 EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<Point> pos, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
+	EffectAnimation(owner, animationName, effects, 1.0f, reversed)
 {
 	positions = pos;
 }
 
 EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
+	EffectAnimation(owner, animationName, effects, 1.0f, reversed)
 {
 	positions.push_back(pos);
 }
 
 EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
+	EffectAnimation(owner, animationName, effects, 1.0f, reversed)
 {
 	assert(hex.isValid());
 	battlehexes.push_back(hex);
@@ -951,6 +952,7 @@ bool EffectAnimation::init()
 	be.effectID = ID;
 	be.animation = animation;
 	be.currentFrame = 0;
+	be.transparencyFactor = transparencyFactor;
 	be.type = reversed ? BattleEffect::AnimType::REVERSE : BattleEffect::AnimType::DEFAULT;
 
 	for (size_t i = 0; i < std::max(battlehexes.size(), positions.size()); ++i)

+ 4 - 3
client/battle/BattleAnimationClasses.h

@@ -309,9 +309,10 @@ public:
 class EffectAnimation : public BattleAnimation
 {
 	std::string soundName;
+	int effectFlags;
+	float transparencyFactor;
 	bool effectFinished;
 	bool reversed;
-	int effectFlags;
 
 	std::shared_ptr<CAnimation>	animation;
 	std::vector<Point> positions;
@@ -335,14 +336,14 @@ public:
 	};
 
 	/// Create animation with screen-wide effect
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, bool reversed = false);
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, float transparencyFactor = 1.f, bool reversed = false);
 
 	/// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset
 	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos                 , int effects = 0, bool reversed = false);
 	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<Point> pos    , int effects = 0, bool reversed = false);
 
 	/// Create animation positioned at certain hex(es)
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex             , int effects = 0, bool reversed = false);
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex             , int effects = 0, float transparencyFactor = 1.0f, bool reversed = false);
 	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<BattleHex> hex, int effects = 0, bool reversed = false);
 
 	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex,   int effects = 0, bool reversed = false);

+ 5 - 4
client/battle/BattleEffectsController.cpp

@@ -44,7 +44,7 @@ void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHe
 	displayEffect(effect, AudioPath(), destTile);
 }
 
-void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile)
+void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile, float transparencyFactor)
 {
 	size_t effectID = static_cast<size_t>(effect);
 
@@ -52,7 +52,7 @@ void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPat
 
 	CCS->soundh->playSound( soundFile );
 
-	owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile));
+	owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile, 0, transparencyFactor));
 }
 
 void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte)
@@ -69,7 +69,7 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 	switch(static_cast<BonusType>(bte.effect))
 	{
 		case BonusType::HP_REGENERATION:
-			displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition());
+			displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition(), 0.5);
 			break;
 		case BonusType::MANA_DRAIN:
 			displayEffect(EBattleEffect::MANA_DRAIN, AudioPath::builtin("MANADRAI"), stack->getPosition());
@@ -78,7 +78,7 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 			displayEffect(EBattleEffect::POISON, AudioPath::builtin("POISON"), stack->getPosition());
 			break;
 		case BonusType::FEAR:
-			displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition());
+			displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition(), 0.5);
 			break;
 		case BonusType::MORALE:
 		{
@@ -124,6 +124,7 @@ void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer
 			currentFrame %= elem.animation->size();
 
 			auto img = elem.animation->getImage(currentFrame, static_cast<size_t>(elem.type));
+			img->setAlpha(255 * elem.transparencyFactor);
 
 			canvas.draw(img, elem.pos);
 		});

+ 3 - 2
client/battle/BattleEffectsController.h

@@ -39,7 +39,8 @@ struct BattleEffect
 
 	AnimType type;
 	Point pos; //position on the screen
-	float currentFrame;
+	float currentFrame = 0.0;
+	float transparencyFactor = 1.0;
 	std::shared_ptr<CAnimation> animation;
 	int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim
 	BattleHex tile; //Indicates if effect which hex the effect is drawn on
@@ -65,7 +66,7 @@ public:
 
 	//displays custom effect on the battlefield
 	void displayEffect(EBattleEffect effect, const BattleHex & destTile);
-	void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile);
+	void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile, float transparencyFactor = 1.f);
 
 	void battleTriggerEffect(const BattleTriggerEffect & bte);
 

+ 1 - 3
client/battle/BattleFieldController.cpp

@@ -114,7 +114,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 
 	//preparing cells and hexes
 	cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY);
-	cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP"), EImageBlitMode::ALPHA);
+	cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP"), EImageBlitMode::SIMPLE);
 	cellUnitMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
 	cellUnitMaxMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
 
@@ -124,8 +124,6 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"), EImageBlitMode::COLORKEY);
 	shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"), EImageBlitMode::COLORKEY);
 
-	cellShade->setShadowEnabled(true);
-
 	if(!owner.siegeController)
 	{
 		auto bfieldType = owner.getBattle()->battleGetBattlefieldType();

+ 2 - 2
client/battle/BattleInterface.cpp

@@ -535,9 +535,9 @@ void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSp
 				flags |= EffectAnimation::SCREEN_FILL;
 
 			if (!destinationTile.isValid())
-				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags));
+				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags, animation.transparency));
 			else
-				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags));
+				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags, animation.transparency));
 		}
 	}
 }

+ 1 - 1
client/battle/BattleInterfaceClasses.cpp

@@ -398,7 +398,7 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her
 	else
 		animationPath = hero->getHeroClass()->imageBattleMale;
 
-	animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::ALPHA);
+	animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::WITH_SHADOW);
 
 	pos.w = 64;
 	pos.h = 136;

+ 4 - 4
client/battle/BattleObstacleController.cpp

@@ -50,11 +50,11 @@ void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi)
 	if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
 	{
 		// obstacle uses single bitmap image for animations
-		obstacleImages[oi.uniqueID] = GH.renderHandler().loadImage(animationName.toType<EResType::IMAGE>(), EImageBlitMode::COLORKEY);
+		obstacleImages[oi.uniqueID] = GH.renderHandler().loadImage(animationName.toType<EResType::IMAGE>(), EImageBlitMode::SIMPLE);
 	}
 	else
 	{
-		obstacleAnimations[oi.uniqueID] = GH.renderHandler().loadAnimation(animationName, EImageBlitMode::COLORKEY);
+		obstacleAnimations[oi.uniqueID] = GH.renderHandler().loadAnimation(animationName, EImageBlitMode::SIMPLE);
 		obstacleImages[oi.uniqueID] = obstacleAnimations[oi.uniqueID]->getImage(0);
 	}
 }
@@ -78,7 +78,7 @@ void BattleObstacleController::obstacleRemoved(const std::vector<ObstacleChanges
 		if(animationPath.empty())
 			continue;
 
-		auto animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::COLORKEY);
+		auto animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::SIMPLE);
 		auto first = animation->getImage(0, 0);
 		if(!first)
 			continue;
@@ -105,7 +105,7 @@ void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<
 		if(!oi->visibleForSide(side, owner.getBattle()->battleHasNativeStack(side)))
 			continue;
 
-		auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation(), EImageBlitMode::ALPHA);
+		auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation(), EImageBlitMode::SIMPLE);
 		auto first = animation->getImage(0, 0);
 		if(!first)
 			continue;

+ 1 - 1
client/battle/BattleStacksController.cpp

@@ -636,7 +636,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 	{
 		owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]()
 		{
-			owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition());
+			owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition(), 0.5);
 		});
 	}
 

+ 5 - 5
client/battle/CreatureAnimation.cpp

@@ -17,6 +17,7 @@
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"
 #include "../render/ColorFilter.h"
+#include "../render/Colors.h"
 #include "../render/IRenderHandler.h"
 
 static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 };
@@ -199,8 +200,8 @@ CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedControll
 	  speedController(controller),
 	  once(false)
 {
-	forward = GH.renderHandler().loadAnimation(name_, EImageBlitMode::ALPHA);
-	reverse = GH.renderHandler().loadAnimation(name_, EImageBlitMode::ALPHA);
+	forward = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_OVERLAY);
+	reverse = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_OVERLAY);
 
 	// if necessary, add one frame into vcmi-only group DEAD
 	if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0)
@@ -339,15 +340,14 @@ void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter,
 
 	if(image)
 	{
-		image->setShadowEnabled(true);
-		image->setOverlayEnabled(isIdle());
 		if (isIdle())
 			image->setOverlayColor(genBorderColor(getBorderStrength(elapsedTime), border));
+		else
+			image->setOverlayColor(Colors::TRANSPARENCY);
 
 		image->adjustPalette(shifter, 0);
 
 		canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h));
-
 	}
 }
 

+ 15 - 22
client/mapView/MapRenderer.cpp

@@ -316,7 +316,7 @@ uint8_t MapRendererBorder::checksum(IMapRendererContext & context, const int3 &
 MapRendererFow::MapRendererFow()
 {
 	fogOfWarFullHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRC"), EImageBlitMode::OPAQUE);
-	fogOfWarPartialHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRE"), EImageBlitMode::ALPHA);
+	fogOfWarPartialHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRE"), EImageBlitMode::SIMPLE);
 
 	static const std::vector<int> rotations = {22, 15, 2, 13, 12, 16, 28, 17, 20, 19, 7, 24, 26, 25, 30, 32, 27};
 
@@ -383,24 +383,25 @@ std::shared_ptr<CAnimation> MapRendererObjects::getBaseAnimation(const CGObjectI
 	}
 
 	bool generateMovementGroups = (info->id == Obj::BOAT) || (info->id == Obj::HERO);
+	bool enableOverlay = obj->ID != Obj::BOAT && obj->ID != Obj::HERO && obj->getOwner() != PlayerColor::UNFLAGGABLE;
 
 	// Boat appearance files only contain single, unanimated image
 	// proper boat animations are actually in different file
 	if (info->id == Obj::BOAT)
 		if(auto boat = dynamic_cast<const CGBoat*>(obj); boat && !boat->actualAnimation.empty())
-			return getAnimation(boat->actualAnimation, generateMovementGroups);
+			return getAnimation(boat->actualAnimation, generateMovementGroups, enableOverlay);
 
-	return getAnimation(info->animationFile, generateMovementGroups);
+	return getAnimation(info->animationFile, generateMovementGroups, enableOverlay);
 }
 
-std::shared_ptr<CAnimation> MapRendererObjects::getAnimation(const AnimationPath & filename, bool generateMovementGroups)
+std::shared_ptr<CAnimation> MapRendererObjects::getAnimation(const AnimationPath & filename, bool generateMovementGroups, bool enableOverlay)
 {
 	auto it = animations.find(filename);
 
 	if(it != animations.end())
 		return it->second;
 
-	auto ret = GH.renderHandler().loadAnimation(filename, EImageBlitMode::ALPHA);
+	auto ret = GH.renderHandler().loadAnimation(filename, enableOverlay ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::WITH_SHADOW);
 	animations[filename] = ret;
 
 	if(generateMovementGroups)
@@ -427,14 +428,14 @@ std::shared_ptr<CAnimation> MapRendererObjects::getFlagAnimation(const CGObjectI
 	{
 		assert(dynamic_cast<const CGHeroInstance *>(obj) != nullptr);
 		assert(obj->tempOwner.isValidPlayer());
-		return getAnimation(AnimationPath::builtin(heroFlags[obj->tempOwner.getNum()]), true);
+		return getAnimation(AnimationPath::builtin(heroFlags[obj->tempOwner.getNum()]), true, false);
 	}
 
 	if(obj->ID == Obj::BOAT)
 	{
 		const auto * boat = dynamic_cast<const CGBoat *>(obj);
 		if(boat && boat->hero && !boat->flagAnimations[boat->hero->tempOwner.getNum()].empty())
-			return getAnimation(boat->flagAnimations[boat->hero->tempOwner.getNum()], true);
+			return getAnimation(boat->flagAnimations[boat->hero->tempOwner.getNum()], true, false);
 	}
 
 	return nullptr;
@@ -447,7 +448,7 @@ std::shared_ptr<CAnimation> MapRendererObjects::getOverlayAnimation(const CGObje
 		// Boats have additional animation with waves around boat
 		const auto * boat = dynamic_cast<const CGBoat *>(obj);
 		if(boat && boat->hero && !boat->overlayAnimation.empty())
-			return getAnimation(boat->overlayAnimation, true);
+			return getAnimation(boat->overlayAnimation, true, false);
 	}
 	return nullptr;
 }
@@ -478,22 +479,14 @@ void MapRendererObjects::renderImage(IMapRendererContext & context, Canvas & tar
 		return;
 
 	image->setAlpha(transparency);
-	image->setShadowEnabled(true);
-	if (object->ID != Obj::HERO)
+	if (object->ID != Obj::HERO) // heroes use separate image with flag instead of player-colored palette
 	{
-		image->setOverlayEnabled(object->getOwner().isValidPlayer() || object->getOwner() == PlayerColor::NEUTRAL);
-
 		if (object->getOwner().isValidPlayer())
 			image->setOverlayColor(graphics->playerColors[object->getOwner().getNum()]);
 
 		if (object->getOwner() == PlayerColor::NEUTRAL)
 			image->setOverlayColor(graphics->neutralColor);
 	}
-	else
-	{
-		// heroes use separate image with flag instead of player-colored palette
-		image->setOverlayEnabled(false);
-	}
 
 	Point offsetPixels = context.objectImageOffset(object->id, coordinates);
 
@@ -567,10 +560,10 @@ uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 &
 }
 
 MapRendererOverlay::MapRendererOverlay()
-	: imageGrid(GH.renderHandler().loadImage(ImagePath::builtin("debug/grid"), EImageBlitMode::ALPHA))
-	, imageBlocked(GH.renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::ALPHA))
-	, imageVisitable(GH.renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::ALPHA))
-	, imageSpellRange(GH.renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::ALPHA))
+	: imageGrid(GH.renderHandler().loadImage(ImagePath::builtin("debug/grid"), EImageBlitMode::COLORKEY))
+	, imageBlocked(GH.renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::COLORKEY))
+	, imageVisitable(GH.renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::COLORKEY))
+	, imageSpellRange(GH.renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::COLORKEY))
 {
 
 }
@@ -626,7 +619,7 @@ uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 &
 }
 
 MapRendererPath::MapRendererPath()
-	: pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"), EImageBlitMode::ALPHA))
+	: pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"), EImageBlitMode::SIMPLE))
 {
 }
 

+ 1 - 1
client/mapView/MapRenderer.h

@@ -77,7 +77,7 @@ class MapRendererObjects
 	std::shared_ptr<CAnimation> getFlagAnimation(const CGObjectInstance * obj);
 	std::shared_ptr<CAnimation> getOverlayAnimation(const CGObjectInstance * obj);
 
-	std::shared_ptr<CAnimation> getAnimation(const AnimationPath & filename, bool generateMovementGroups);
+	std::shared_ptr<CAnimation> getAnimation(const AnimationPath & filename, bool generateMovementGroups, bool enableOverlay);
 
 	std::shared_ptr<IImage> getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr<CAnimation> & animation) const;
 

+ 23 - 6
client/render/IImage.h

@@ -37,9 +37,29 @@ enum class EImageBlitMode : uint8_t
 	/// RGBA: full alpha transparency range, e.g. shadows
 	COLORKEY,
 
-	/// Should be avoided if possible, use only for images that use def's with semi-transparency
-	/// Indexed or RGBA: Image might have full alpha transparency range, e.g. shadows
-	ALPHA
+	/// Full transparency including shadow, but treated as a single image
+	/// Indexed: Image can have alpha transparency, e.g. shadow
+	/// RGBA: full alpha transparency range, e.g. shadows
+	/// Upscaled form: single image, no option to display shadow separately
+	SIMPLE,
+
+	/// RGBA, may consist from 2 separate parts: base and shadow, overlay not preset or treated as part of body
+	WITH_SHADOW,
+
+	/// RGBA, may consist from 3 separate parts: base, shadow, and overlay
+	WITH_SHADOW_AND_OVERLAY,
+
+	/// RGBA, contains only body, with shadow and overlay disabled
+	ONLY_BODY,
+
+	/// RGBA, contains only body, with shadow disabled and overlay treated as part of body
+	ONLY_BODY_IGNORE_OVERLAY,
+
+	/// RGBA, contains only shadow
+	ONLY_SHADOW,
+
+	/// RGBA, contains only overlay
+	ONLY_OVERLAY,
 };
 
 /// Base class for images for use in client code.
@@ -75,9 +95,6 @@ public:
 	//only indexed bitmaps with 7 special colors
 	virtual void setOverlayColor(const ColorRGBA & color) = 0;
 
-	virtual void setShadowEnabled(bool on) = 0;
-	virtual void setBodyEnabled(bool on) = 0;
-	virtual void setOverlayEnabled(bool on) = 0;
 	virtual std::shared_ptr<const ISharedImage> getSharedImage() const = 0;
 
 	virtual ~IImage() = default;

+ 6 - 2
client/render/ImageLocator.cpp

@@ -124,8 +124,12 @@ std::string ImageLocator::toString() const
 	if (playerColored.isValidPlayer())
 		result += "-player" + playerColored.toString();
 
-	if (layer != EImageLayer::ALL)
-		result += "-layer" + std::to_string(static_cast<int>(layer));
+	if (layer == EImageBlitMode::ONLY_OVERLAY)
+		result += "-overlay";
+
+	if (layer == EImageBlitMode::ONLY_SHADOW)
+		result += "-shadow";
+
 
 	return result;
 }

+ 4 - 11
client/render/ImageLocator.h

@@ -9,18 +9,11 @@
  */
 #pragma once
 
+#include "IImage.h"
+
 #include "../../lib/filesystem/ResourcePath.h"
 #include "../../lib/constants/EntityIdentifiers.h"
 
-enum class EImageLayer
-{
-	ALL,
-
-	BODY,
-	SHADOW,
-	OVERLAY,
-};
-
 struct ImageLocator
 {
 	std::optional<ImagePath> image;
@@ -28,13 +21,13 @@ struct ImageLocator
 	int defFrame = -1;
 	int defGroup = -1;
 
-	PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE;
+	PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE; // FIXME: treat as identical to blue to avoid double-loading?
 
 	bool verticalFlip = false;
 	bool horizontalFlip = false;
 	int8_t scalingFactor = 0; // 0 = auto / use default scaling
 	int8_t preScaledFactor = 1;
-	EImageLayer layer = EImageLayer::ALL;
+	EImageBlitMode layer = EImageBlitMode::OPAQUE;
 
 	ImageLocator() = default;
 	ImageLocator(const AnimationPath & path, int frame, int group);

+ 52 - 33
client/renderSDL/ImageScaled.cpp

@@ -28,9 +28,7 @@ ImageScaled::ImageScaled(const ImageLocator & inputLocator, const std::shared_pt
 	, alphaValue(SDL_ALPHA_OPAQUE)
 	, blitMode(mode)
 {
-	setBodyEnabled(true);
-	if (mode == EImageBlitMode::ALPHA)
-		setShadowEnabled(true);
+	prepareImages();
 }
 
 std::shared_ptr<const ISharedImage> ImageScaled::getSharedImage() const
@@ -92,8 +90,7 @@ void ImageScaled::setOverlayColor(const ColorRGBA & color)
 void ImageScaled::playerColored(PlayerColor player)
 {
 	playerColor = player;
-	if (body)
-		setBodyEnabled(true); // regenerate
+	prepareImages();
 }
 
 void ImageScaled::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
@@ -106,41 +103,63 @@ void ImageScaled::adjustPalette(const ColorFilter &shifter, uint32_t colorsToSki
 	// TODO: implement
 }
 
-void ImageScaled::setShadowEnabled(bool on)
+void ImageScaled::prepareImages()
 {
-	assert(blitMode == EImageBlitMode::ALPHA);
-	if (on)
+	switch(blitMode)
 	{
-		locator.layer = EImageLayer::SHADOW;
-		locator.playerColored = PlayerColor::CANNOT_DETERMINE;
-		shadow = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+		case EImageBlitMode::OPAQUE:
+		case EImageBlitMode::COLORKEY:
+		case EImageBlitMode::SIMPLE:
+			locator.layer = blitMode;
+			locator.playerColored = playerColor;
+			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+			break;
+
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+		case EImageBlitMode::ONLY_BODY:
+			locator.layer = EImageBlitMode::ONLY_BODY;
+			locator.playerColored = playerColor;
+			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+			break;
+
+		case EImageBlitMode::WITH_SHADOW:
+		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
+			locator.layer = EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY;
+			locator.playerColored = playerColor;
+			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+			break;
+
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::ONLY_OVERLAY:
+			body = nullptr;
+			break;
 	}
-	else
-		shadow = nullptr;
-}
 
-void ImageScaled::setBodyEnabled(bool on)
-{
-	if (on)
+	switch(blitMode)
 	{
-		locator.layer = blitMode == EImageBlitMode::ALPHA ? EImageLayer::BODY : EImageLayer::ALL;
-		locator.playerColored = playerColor;
-		body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+		case EImageBlitMode::SIMPLE:
+		case EImageBlitMode::WITH_SHADOW:
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			locator.layer = EImageBlitMode::ONLY_SHADOW;
+			locator.playerColored = PlayerColor::CANNOT_DETERMINE;
+			shadow = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+			break;
+		default:
+			shadow = nullptr;
+			break;
 	}
-	else
-		body = nullptr;
-}
-
 
-void ImageScaled::setOverlayEnabled(bool on)
-{
-	assert(blitMode == EImageBlitMode::ALPHA);
-	if (on)
+	switch(blitMode)
 	{
-		locator.layer = EImageLayer::OVERLAY;
-		locator.playerColored = PlayerColor::CANNOT_DETERMINE;
-		overlay = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+		case EImageBlitMode::ONLY_OVERLAY:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			locator.layer = EImageBlitMode::ONLY_OVERLAY;
+			locator.playerColored = PlayerColor::CANNOT_DETERMINE;
+			overlay = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+			break;
+		default:
+			overlay = nullptr;
+			break;
 	}
-	else
-		overlay = nullptr;
 }

+ 1 - 3
client/renderSDL/ImageScaled.h

@@ -44,6 +44,7 @@ private:
 	uint8_t alphaValue;
 	EImageBlitMode blitMode;
 
+	void prepareImages();
 public:
 	ImageScaled(const ImageLocator & locator, const std::shared_ptr<const ISharedImage> & source, EImageBlitMode mode);
 
@@ -60,8 +61,5 @@ public:
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
 
-	void setShadowEnabled(bool on) override;
-	void setBodyEnabled(bool on) override;
-	void setOverlayEnabled(bool on) override;
 	std::shared_ptr<const ISharedImage> getSharedImage() const override;
 };

+ 5 - 13
client/renderSDL/RenderHandler.cpp

@@ -303,22 +303,14 @@ std::shared_ptr<const ISharedImage> RenderHandler::scaleImage(const ImageLocator
 	if (imageFiles.count(locator))
 		return imageFiles.at(locator);
 
-	auto handle = image->createImageReference(locator.layer == EImageLayer::ALL ? EImageBlitMode::OPAQUE : EImageBlitMode::ALPHA);
+	auto handle = image->createImageReference(locator.layer);
 
 	assert(locator.scalingFactor != 1); // should be filtered-out before
-
-	handle->setBodyEnabled(locator.layer == EImageLayer::ALL || locator.layer == EImageLayer::BODY);
-	if (locator.layer != EImageLayer::ALL)
-	{
-		handle->setOverlayEnabled(locator.layer == EImageLayer::OVERLAY);
-		handle->setShadowEnabled( locator.layer == EImageLayer::SHADOW);
-	}
-	if (locator.layer == EImageLayer::ALL && locator.playerColored != PlayerColor::CANNOT_DETERMINE)
+	if (locator.playerColored != PlayerColor::CANNOT_DETERMINE)
 		handle->playerColored(locator.playerColored);
 
 	handle->scaleInteger(locator.scalingFactor);
 
-	// TODO: try to optimize image size (possibly even before scaling?) - trim image borders if they are completely transparent
 	auto result = handle->getSharedImage();
 	storeCachedImage(locator, result);
 	return result;
@@ -331,9 +323,9 @@ std::shared_ptr<IImage> RenderHandler::loadImage(const ImageLocator & locator, E
 	if(adjustedLocator.image)
 	{
 		std::string imgPath = (*adjustedLocator.image).getName();
-		if(adjustedLocator.layer == EImageLayer::OVERLAY)
+		if(adjustedLocator.layer == EImageBlitMode::ONLY_OVERLAY)
 			imgPath += "-OVERLAY";
-		if(adjustedLocator.layer == EImageLayer::SHADOW)
+		if(adjustedLocator.layer == EImageBlitMode::ONLY_SHADOW)
 			imgPath += "-SHADOW";
 
 		if(CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath)) ||
@@ -394,7 +386,7 @@ std::shared_ptr<IImage> RenderHandler::loadImage(const ImagePath & path, EImageB
 
 std::shared_ptr<IImage> RenderHandler::createImage(SDL_Surface * source)
 {
-	return std::make_shared<SDLImageShared>(source)->createImageReference(EImageBlitMode::ALPHA);
+	return std::make_shared<SDLImageShared>(source)->createImageReference(EImageBlitMode::SIMPLE);
 }
 
 std::shared_ptr<CAnimation> RenderHandler::loadAnimation(const AnimationPath & path, EImageBlitMode mode)

+ 46 - 51
client/renderSDL/SDLImage.cpp

@@ -139,6 +139,8 @@ SDLImageShared::SDLImageShared(const ImagePath & filename, int preScaleFactor)
 		savePalette();
 		fullSize.x = surf->w;
 		fullSize.y = surf->h;
+
+		optimizeSurface();
 	}
 }
 
@@ -180,7 +182,7 @@ void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Poin
 	if (palette && surf->format->palette)
 		SDL_SetSurfacePalette(surf, palette);
 
-	if(surf->format->palette && mode == EImageBlitMode::ALPHA)
+	if(surf->format->palette && mode != EImageBlitMode::OPAQUE && mode != EImageBlitMode::COLORKEY)
 	{
 		CSDL_Ext::blit8bppAlphaTo24bpp(surf, sourceRect, where, destShift, alpha);
 	}
@@ -261,6 +263,13 @@ void SDLImageShared::optimizeSurface()
 		SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE);
 		SDL_BlitSurface(surf, &rectSDL, newSurface, nullptr);
 
+		if (SDL_HasColorKey(surf))
+		{
+			uint32_t colorKey;
+			SDL_GetColorKey(surf, &colorKey);
+			SDL_SetColorKey(newSurface, SDL_TRUE, colorKey);
+		}
+
 		SDL_FreeSurface(surf);
 		surf = newSurface;
 
@@ -357,7 +366,7 @@ void SDLImageIndexed::playerColored(PlayerColor player)
 bool SDLImageShared::isTransparent(const Point & coords) const
 {
 	if (surf)
-		return CSDL_Ext::isTransparent(surf, coords.x, coords.y);
+		return CSDL_Ext::isTransparent(surf, coords.x - margins.x, coords.y	- margins.y);
 	else
 		return true;
 }
@@ -425,7 +434,7 @@ void SDLImageIndexed::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove,
 void SDLImageIndexed::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
 {
 	// If shadow is enabled, following colors must be skipped unconditionally
-	if (shadowEnabled)
+	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY)
 		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);
 
 	// Note: here we skip first colors in the palette that are predefined in H3 images
@@ -445,15 +454,10 @@ SDLImageIndexed::SDLImageIndexed(const std::shared_ptr<const ISharedImage> & ima
 	:SDLImageBase::SDLImageBase(image, mode)
 	,originalPalette(originalPalette)
 {
-
 	currentPalette = SDL_AllocPalette(originalPalette->ncolors);
 	SDL_SetPaletteColors(currentPalette, originalPalette->colors, 0, originalPalette->ncolors);
 
-	if (mode == EImageBlitMode::ALPHA)
-	{
-		setOverlayColor(Colors::TRANSPARENCY);
-		setShadowTransparency(1.0);
-	}
+	preparePalette();
 }
 
 SDLImageIndexed::~SDLImageIndexed()
@@ -500,36 +504,42 @@ void SDLImageIndexed::setOverlayColor(const ColorRGBA & color)
 	}
 }
 
-void SDLImageIndexed::setShadowEnabled(bool on)
-{
-	if (on)
-		setShadowTransparency(1.0);
-
-	if (!on && blitMode == EImageBlitMode::ALPHA)
-		setShadowTransparency(0.0);
-
-	shadowEnabled = on;
-}
-
-void SDLImageIndexed::setBodyEnabled(bool on)
-{
-	if (on)
-		adjustPalette(ColorFilter::genEmptyShifter(), 0);
-	else
-		adjustPalette(ColorFilter::genAlphaShifter(0), 0);
-
-	bodyEnabled = on;
-}
-
-void SDLImageIndexed::setOverlayEnabled(bool on)
+void SDLImageIndexed::preparePalette()
 {
-	if (on)
-		setOverlayColor(Colors::WHITE_TRUE);
+	switch(blitMode)
+	{
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::ONLY_OVERLAY:
+			adjustPalette(ColorFilter::genAlphaShifter(0), 0);
+			break;
+	}
 
-	if (!on && blitMode == EImageBlitMode::ALPHA)
-		setOverlayColor(Colors::TRANSPARENCY);
+	switch(blitMode)
+	{
+		case EImageBlitMode::SIMPLE:
+		case EImageBlitMode::WITH_SHADOW:
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			setShadowTransparency(1.0);
+			break;
+		case EImageBlitMode::ONLY_BODY:
+		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
+		case EImageBlitMode::ONLY_OVERLAY:
+			setShadowTransparency(0.0);
+			break;
+	}
 
-	overlayEnabled = on;
+	switch(blitMode)
+	{
+		case EImageBlitMode::ONLY_OVERLAY:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			setOverlayColor(Colors::WHITE_TRUE);
+			break;
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::ONLY_BODY:
+			setOverlayColor(Colors::TRANSPARENCY);
+			break;
+	}
 }
 
 SDLImageShared::~SDLImageShared()
@@ -609,21 +619,6 @@ void SDLImageBase::setBlitMode(EImageBlitMode mode)
 	blitMode = mode;
 }
 
-void SDLImageRGB::setShadowEnabled(bool on)
-{
-	// Not supported. Theoretically we can try to extract all pixels of specific colors, but better to use 8-bit images or composite images
-}
-
-void SDLImageRGB::setBodyEnabled(bool on)
-{
-	// Not supported. Theoretically we can try to extract all pixels of specific colors, but better to use 8-bit images or composite images
-}
-
-void SDLImageRGB::setOverlayEnabled(bool on)
-{
-	// Not supported. Theoretically we can try to extract all pixels of specific colors, but better to use 8-bit images or composite images
-}
-
 void SDLImageRGB::setOverlayColor(const ColorRGBA & color)
 {}
 

+ 1 - 12
client/renderSDL/SDLImage.h

@@ -89,11 +89,8 @@ class SDLImageIndexed final : public SDLImageBase
 	SDL_Palette * currentPalette = nullptr;
 	SDL_Palette * originalPalette = nullptr;
 
-	bool bodyEnabled = true;
-	bool shadowEnabled = false;
-	bool overlayEnabled = false;
-
 	void setShadowTransparency(float factor);
+	void preparePalette();
 public:
 	SDLImageIndexed(const std::shared_ptr<const ISharedImage> & image, SDL_Palette * palette, EImageBlitMode mode);
 	~SDLImageIndexed();
@@ -106,10 +103,6 @@ public:
 	void scaleInteger(int factor) override;
 	void scaleTo(const Point & size) override;
 	void exportBitmap(const boost::filesystem::path & path) const override;
-
-	void setShadowEnabled(bool on) override;
-	void setBodyEnabled(bool on) override;
-	void setOverlayEnabled(bool on) override;
 };
 
 class SDLImageRGB final : public SDLImageBase
@@ -125,8 +118,4 @@ public:
 	void scaleInteger(int factor) override;
 	void scaleTo(const Point & size) override;
 	void exportBitmap(const boost::filesystem::path & path) const override;
-
-	void setShadowEnabled(bool on) override;
-	void setBodyEnabled(bool on) override;
-	void setOverlayEnabled(bool on) override;
 };

+ 1 - 1
client/renderSDL/SDL_Extensions.cpp

@@ -90,7 +90,7 @@ SDL_Surface * CSDL_Ext::newSurface(const Point & dimensions, SDL_Surface * mod)
 	if (mod->format->palette)
 	{
 		assert(ret->format->palette);
-		assert(ret->format->palette->ncolors == mod->format->palette->ncolors);
+		assert(ret->format->palette->ncolors >= mod->format->palette->ncolors);
 		memcpy(ret->format->palette->colors, mod->format->palette->colors, mod->format->palette->ncolors * sizeof(SDL_Color));
 	}
 	return ret;

+ 3 - 3
client/widgets/CGarrisonInt.cpp

@@ -274,12 +274,12 @@ bool CGarrisonSlot::mustForceReselection() const
 	if (!LOCPLINT->makingTurn)
 		return true;
 
-	if (!creature || !selection->creature)
-		return false;
-
 	// Attempt to take creatures from ally (select theirs first)
 	if (!selection->our())
 		return true;
+	
+	if (!creature || !selection->creature)
+		return false;
 
 	// Attempt to swap creatures with ally (select ours first)
 	if (selection->creature != creature && withAlly)

+ 5 - 5
client/widgets/Images.cpp

@@ -20,6 +20,7 @@
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"
 #include "../render/ColorFilter.h"
+#include "../render/Colors.h"
 
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleInterfaceClasses.h"
@@ -194,12 +195,12 @@ CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, i
 {
 	pos.x += x;
 	pos.y += y;
-	anim = GH.renderHandler().loadAnimation(name, EImageBlitMode::COLORKEY);
+	anim = GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY);
 	init();
 }
 
 CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, Rect targetPos, size_t Group, ui8 Flags):
-	anim(GH.renderHandler().loadAnimation(name, EImageBlitMode::COLORKEY)),
+	anim(GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)),
 	frame(Frame),
 	group(Group),
 	flags(Flags),
@@ -317,7 +318,7 @@ bool CAnimImage::isPlayerColored() const
 }
 
 CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha):
-	anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::ALPHA : EImageBlitMode::COLORKEY)),
+	anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)),
 	group(Group),
 	frame(0),
 	first(0),
@@ -430,9 +431,8 @@ void CShowableAnim::blitImage(size_t frame, size_t group, Canvas & to)
 	auto img = anim->getImage(frame, group);
 	if(img)
 	{
-		if (flags & CREATURE_MODE)
-			img->setShadowEnabled(true);
 		img->setAlpha(alpha);
+		img->setOverlayColor(Colors::TRANSPARENCY);
 		to.draw(img, pos.topLeft(), src);
 	}
 }

+ 1 - 1
client/windows/CCastleInterface.cpp

@@ -98,7 +98,7 @@ CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town
 		border = GH.renderHandler().loadImage(str->borderName, EImageBlitMode::COLORKEY);
 
 	if(!str->areaName.empty())
-		area = GH.renderHandler().loadImage(str->areaName, EImageBlitMode::ALPHA);
+		area = GH.renderHandler().loadImage(str->areaName, EImageBlitMode::SIMPLE);
 }
 
 const CBuilding * CBuildingRect::getBuilding()

+ 2 - 1
client/windows/CHeroWindow.cpp

@@ -199,10 +199,11 @@ void CHeroWindow::update()
 		OBJECT_CONSTRUCTION;
 		if(!garr)
 		{
+			bool removableTroops = curHero->getOwner() == LOCPLINT->playerID;
 			std::string helpBox = heroscrn[32];
 			boost::algorithm::replace_first(helpBox, "%s", CGI->generaltexth->allTexts[43]);
 
-			garr = std::make_shared<CGarrisonInt>(Point(15, 485), 8, Point(), curHero);
+			garr = std::make_shared<CGarrisonInt>(Point(15, 485), 8, Point(), curHero, nullptr, removableTroops);
 			auto split = std::make_shared<CButton>(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [this](){ garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT);
 			garr->addSplitBtn(split);
 		}

+ 1 - 1
client/windows/GUIClasses.cpp

@@ -950,7 +950,7 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, BuildingID bu
 	}
 	else if(auto uni = dynamic_cast<const CGUniversity *>(_market); uni->appearance)
 	{
-		titlePic = std::make_shared<CAnimImage>(uni->appearance->animationFile, 0);
+		titlePic = std::make_shared<CAnimImage>(uni->appearance->animationFile, 0, 0, 0, 0, CShowableAnim::CREATURE_MODE);
 		titleStr = uni->title;
 		speechStr = uni->speech;
 	}

+ 2 - 1
config/schemas/spell.json

@@ -22,7 +22,8 @@
 						"properties" : {
 							"verticalPosition" : {"type" : "string", "enum" :["top","bottom"]},
 							"defName" : {"type" : "string", "format" : "animationFile"},
-							"effectName" : { "type" : "string" }
+							"effectName" : { "type" : "string" },
+							"transparency" : {"type" : "number", "minimum" : 0, "maximum" : 1}
 						},
 						"additionalProperties" : false
 					}

+ 1 - 1
config/spells/ability.json

@@ -252,7 +252,7 @@
 		"targetType": "NO_TARGET",
 
 		"animation":{
-			"hit":["SP04_"]
+			"hit":[{ "defName" : "SP04_", "transparency" : 0.5}]
 		},
 		"sounds": {
 			"cast": "DEATHCLD"

+ 2 - 2
config/spells/offensive.json

@@ -44,7 +44,7 @@
 				{"minimumAngle": 1.20 ,"defName":"C08SPW1"},
 				{"minimumAngle": 1.50 ,"defName":"C08SPW0"}
 			],
-			"hit":["C08SPW5"]
+			"hit":[ {"defName" : "C08SPW5", "transparency" : 0.5 }]
 		},
 		"sounds": {
 			"cast": "ICERAY"
@@ -309,7 +309,7 @@
 		"targetType" : "CREATURE",
 
 		"animation":{
-			"affect":["C14SPA0"]
+			"affect":[{"defName" : "C14SPA0", "transparency" : 0.5}]
 		},
 		"sounds": {
 			"cast": "SACBRETH"

+ 1 - 1
config/spells/other.json

@@ -483,7 +483,7 @@
 		"targetType" : "CREATURE",
 
 		"animation":{
-			"affect":["C01SPE0"]
+			"affect":[{ "defName" : "C01SPE0", "transparency" : 0.5}]
 		},
 		"sounds": {
 			"cast": "RESURECT"

+ 2 - 2
config/spells/timed.json

@@ -652,7 +652,7 @@
 		"targetType" : "CREATURE",
 
 		"animation":{
-			"affect":["C07SPA1"],
+			"affect":[{"defName" : "C07SPA1", "transparency" : 0.5}],
 			"projectile":[{"defName":"C07SPA0"}]//???
 		},
 		"sounds": {
@@ -696,7 +696,7 @@
 		"targetType" : "CREATURE",
 
 		"animation":{
-			"affect":[{"defName":"C10SPW", "verticalPosition":"bottom"}]
+			"affect":[{"defName":"C10SPW", "verticalPosition":"bottom", "transparency" : 0.5}]
 		},
 		"sounds": {
 			"cast": "PRAYER"

+ 1 - 1
docs/modders/Entities_Format/Spell_Format.md

@@ -167,7 +167,7 @@ TODO
 	],
 	"cast" : []
 	"hit":["C20SPX"],
-	"affect":[{"defName":"C03SPA0", "verticalPosition":"bottom"}, "C11SPA1"]
+	"affect":[{"defName":"C03SPA0", "verticalPosition":"bottom", "transparency" : 0.5}, "C11SPA1"]
 }
 ```
 

+ 10 - 5
docs/modders/HD_Graphics.md

@@ -1,10 +1,10 @@
 # HD Graphics
 
-It's possible to provide alternative HD-Graphics within mods. They will be used if any upscaling filter is activated.
+It's possible to provide alternative high-definition graphics within mods. They will be used if any upscaling filter is activated.
 
 ## Preconditions
 
-It's still necessary to add 1x graphics as before. HD graphics are seperate from usual graphics. This allows to partitially use HD for a few graphics in mod. And avoid handling huge graphics if upscaling isn't enabled.
+It's still necessary to add 1x standard definition graphics as before. HD graphics are seperate from usual graphics. This allows to partitially use HD for a few graphics in mod. And avoid handling huge graphics if upscaling isn't enabled.
 
 Currently following scaling factors are possible to use: 2x, 3x, 4x. You can also provide multiple of them (increases size of mod, but improves loading performance for player). It's recommend to provide 2x and 3x images.
 
@@ -20,10 +20,15 @@ The sprites should have the same name and folder structure as in `sprites` and `
 
 ### Shadows / Overlays
 
-It's also possible (but not necessary) to add HD shadows: Just place a image next to the normal upscaled image with the suffix `-shadow`. E.g. `TestImage.png` and `TestImage-shadow.png`.
+It's also possible (but not necessary) to add high-definition shadows: Just place a image next to the normal upscaled image with the suffix `-shadow`. E.g. `TestImage.png` and `TestImage-shadow.png`.
+In future, such shadows will likely become required to correctly exclude shadow from effects such as Clone spell.
+
+Shadow images are used only for animations of following objects:
+- All adventure map objects
+- All creature animations in combat
 
 Same for overlays with `-overlay`. But overlays are **necessary** for some animation graphics. They will be colorized by VCMI.
 
 Currently needed for:
-- flaggable adventure map objects (needs a transparent image with white flags on it)
-- creature battle animations (needs a transparent image with white outline of creature for highlighting on mouse hover)
+- Flaggable adventure map objects. Overlay must contain a transparent image with white flags on it and will be used to colorize flags to owning player
+- Creature battle animations, idle and mouse hover group. Overlay must contain a transparent image with white outline of creature for highlighting on mouse hover)

+ 2 - 2
lib/CGameInfoCallback.cpp

@@ -957,12 +957,12 @@ void CGameInfoCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo &
 
 const CArtifactInstance * CGameInfoCallback::getArtInstance( ArtifactInstanceID aid ) const
 {
-	return gs->map->artInstances[aid.num];
+	return gs->map->artInstances.at(aid.num);
 }
 
 const CGObjectInstance * CGameInfoCallback::getObjInstance( ObjectInstanceID oid ) const
 {
-	return gs->map->objects[oid.num];
+	return gs->map->objects.at(oid.num);
 }
 
 const CArtifactSet * CGameInfoCallback::getArtSet(const ArtifactLocation & loc) const

+ 12 - 8
lib/StartInfo.cpp

@@ -90,18 +90,22 @@ std::string StartInfo::getCampaignName() const
 		return VLC->generaltexth->allTexts[508];
 }
 
-bool StartInfo::isSteadwickFallCampaignMission() const
+bool StartInfo::isRestorationOfErathiaCampaign() const
 {
-	if (!campState)
-		return false;
+	constexpr std::array roeCampaigns = {
+		"DATA/GOOD1",
+		"DATA/EVIL1",
+		"DATA/GOOD2",
+		"DATA/NEUTRAL1",
+		"DATA/EVIL2",
+		"DATA/GOOD3",
+		"DATA/SECRET1",
+	};
 
-	if (campState->getFilename() != "DATA/EVIL1")
-		return false;
-
-	if (campState->currentScenario() != CampaignScenarioID(2))
+	if (!campState)
 		return false;
 
-	return true;
+	return vstd::contains(roeCampaigns, campState->getFilename());
 }
 
 void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const

+ 2 - 2
lib/StartInfo.h

@@ -164,8 +164,8 @@ struct DLL_LINKAGE StartInfo : public Serializeable
 	// TODO: Must be client-side
 	std::string getCampaignName() const;
 
-	/// Controls hardcoded check for "Steadwick's Fall" scenario from "Dungeon and Devils" campaign
-	bool isSteadwickFallCampaignMission() const;
+	/// Controls hardcoded check for handling of garrisons by AI in Restoration of Erathia campaigns to match H3 behavior
+	bool isRestorationOfErathiaCampaign() const;
 
 	template <typename Handler>
 	void serialize(Handler &h)

+ 8 - 17
lib/entities/hero/CHeroClass.cpp

@@ -23,29 +23,20 @@ SecondarySkill CHeroClass::chooseSecSkill(const std::set<SecondarySkill> & possi
 {
 	assert(!possibles.empty());
 
-	if (possibles.size() == 1)
-		return *possibles.begin();
+	std::vector<int> weights;
+	std::vector<SecondarySkill> skills;
 
-	int totalProb = 0;
-	for(const auto & possible : possibles)
-		if (secSkillProbability.count(possible) != 0)
-			totalProb += secSkillProbability.at(possible);
-
-	if (totalProb == 0) // may trigger if set contains only banned skills (0 probability)
-		return *RandomGeneratorUtil::nextItem(possibles, rand);
-
-	auto ran = rand.nextInt(totalProb - 1);
 	for(const auto & possible : possibles)
 	{
+		skills.push_back(possible);
 		if (secSkillProbability.count(possible) != 0)
-			ran -= secSkillProbability.at(possible);
-
-		if(ran < 0)
-			return possible;
+			weights.push_back(secSkillProbability.at(possible));
+		else
+			weights.push_back(1); // H3 behavior - banned skills have minimal (1) chance to be picked
 	}
 
-	assert(0); // should not be possible
-	return *possibles.begin();
+	int selectedIndex = RandomGeneratorUtil::nextItemWeighted(weights, rand);
+	return skills.at(selectedIndex);
 }
 
 bool CHeroClass::isMagicHero() const

+ 7 - 4
lib/mapObjectConstructors/DwellingInstanceConstructor.cpp

@@ -88,25 +88,28 @@ void DwellingInstanceConstructor::randomizeObject(CGDwelling * dwelling, vstd::R
 			dwelling->creatures.back().second.push_back(cre->getId());
 	}
 
-	bool guarded = false; //TODO: serialize for sanity
+	bool guarded = false;
 
-	if(guards.getType() == JsonNode::JsonType::DATA_BOOL) //simple switch
+	if(guards.getType() == JsonNode::JsonType::DATA_BOOL)
 	{
+		//simple switch
 		if(guards.Bool())
 		{
 			guarded = true;
 		}
 	}
-	else if(guards.getType() == JsonNode::JsonType::DATA_VECTOR) //custom guards (eg. Elemental Conflux)
+	else if(guards.getType() == JsonNode::JsonType::DATA_VECTOR)
 	{
+		//custom guards (eg. Elemental Conflux)
 		JsonRandom::Variables emptyVariables;
 		for(auto & stack : randomizer.loadCreatures(guards, rng, emptyVariables))
 		{
 			dwelling->putStack(SlotID(dwelling->stacksCount()), new CStackInstance(stack.getId(), stack.count));
 		}
 	}
-	else //default condition - creatures are of level 5 or higher
+	else if (dwelling->ID == Obj::CREATURE_GENERATOR1 || dwelling->ID == Obj::CREATURE_GENERATOR4)
 	{
+		//default condition - this is dwelling with creatures of level 5 or higher
 		for(auto creatureEntry : availableCreatures)
 		{
 			if(creatureEntry.at(0)->getLevel() >= 5)

+ 2 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -338,7 +338,8 @@ void CGHeroInstance::setHeroType(HeroTypeID heroType)
 
 void CGHeroInstance::initObj(vstd::RNG & rand)
 {
-	updateAppearance();
+	if (ID == Obj::HERO)
+		updateAppearance();
 }
 
 void CGHeroInstance::initHero(vstd::RNG & rand, const HeroTypeID & SUBID)

+ 8 - 2
lib/spells/CSpellHandler.cpp

@@ -544,7 +544,8 @@ void CSpell::serializeJson(JsonSerializeFormat & handler)
 ///CSpell::AnimationInfo
 CSpell::AnimationItem::AnimationItem() :
 	verticalPosition(VerticalPosition::TOP),
-	pause(0)
+	pause(0),
+	transparency(1)
 {
 
 }
@@ -965,10 +966,15 @@ std::shared_ptr<CSpell> CSpellHandler::loadFromJson(const std::string & scope, c
 				auto vPosStr = item["verticalPosition"].String();
 				if("bottom" == vPosStr)
 					newItem.verticalPosition = VerticalPosition::BOTTOM;
+
+				if (item["transparency"].isNumber())
+					newItem.transparency = item["transparency"].Float();
+				else
+					newItem.transparency = 1.0;
 			}
 			else if(item.isNumber())
 			{
-				newItem.pause = static_cast<int>(item.Float());
+				newItem.pause = item.Integer();
 			}
 
 			q.push_back(newItem);

+ 1 - 0
lib/spells/CSpellHandler.h

@@ -74,6 +74,7 @@ public:
 		AnimationPath resourceName;
 		std::string effectName;
 		VerticalPosition verticalPosition;
+		float transparency;
 		int pause;
 
 		AnimationItem();

+ 7 - 6
server/CGameHandler.cpp

@@ -1861,12 +1861,8 @@ bool CGameHandler::bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner
 
 bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player)
 {
-	const CArmedInstance * s1 = static_cast<const CArmedInstance *>(getObjInstance(id1));
-	const CArmedInstance * s2 = static_cast<const CArmedInstance *>(getObjInstance(id2));
-	const CCreatureSet & S1 = *s1;
-	const CCreatureSet & S2 = *s2;
-	StackLocation sl1(s1, p1);
-	StackLocation sl2(s2, p2);
+	const CArmedInstance * s1 = static_cast<const CArmedInstance *>(getObj(id1));
+	const CArmedInstance * s2 = static_cast<const CArmedInstance *>(getObj(id2));
 
 	if (s1 == nullptr || s2 == nullptr)
 	{
@@ -1874,6 +1870,11 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8
 		return false;
 	}
 
+	const CCreatureSet & S1 = *s1;
+	const CCreatureSet & S2 = *s2;
+	StackLocation sl1(s1, p1);
+	StackLocation sl2(s2, p2);
+
 	if (!sl1.slot.validSlot()  ||  !sl2.slot.validSlot())
 	{
 		complain(complainInvalidSlot);