From 28a8d4f4a1edb6cc339aaef72d68e998d274b28e Mon Sep 17 00:00:00 2001
From: Ivan Savenko <saven.ivan@gmail.com>
Date: Sat, 8 Feb 2025 14:47:01 +0000
Subject: [PATCH] Implemented bloodlust, clone, and petrify effects for xbrz
 mode

---
 client/battle/BattleAnimationClasses.cpp  | 21 +++++---
 client/battle/BattleAnimationClasses.h    |  5 +-
 client/battle/BattleEffectsController.cpp |  3 +-
 client/battle/BattleEffectsController.h   |  9 +++-
 client/battle/BattleStacksController.cpp  | 22 ++++----
 client/battle/BattleStacksController.h    |  7 +--
 client/battle/CreatureAnimation.cpp       | 14 ++---
 client/battle/CreatureAnimation.h         |  5 +-
 client/render/CanvasImage.h               |  1 +
 client/render/ColorFilter.cpp             | 37 -------------
 client/render/ColorFilter.h               |  9 ----
 client/render/IImage.h                    |  3 +-
 client/renderSDL/ScalableImage.cpp        | 64 +++++++++++++++++++++--
 client/renderSDL/ScalableImage.h          |  6 +++
 config/battleEffects.json                 | 56 +++++++-------------
 lib/Color.h                               | 14 +++++
 16 files changed, 151 insertions(+), 125 deletions(-)

diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp
index 196066d1f..b5e174b77 100644
--- a/client/battle/BattleAnimationClasses.cpp
+++ b/client/battle/BattleAnimationClasses.cpp
@@ -594,16 +594,19 @@ void ColorTransformAnimation::tick(uint32_t msPassed)
 	if (index == timePoints.size())
 	{
 		//end of animation. Apply ColorShifter using final values and die
-		const auto & shifter = steps[index - 1];
-		owner.stacksController->setStackColorFilter(shifter, stack, spell, false);
+		const auto & lastColor = effectColors[index - 1];
+		const auto & lastAlpha = transparency[index - 1];
+		owner.stacksController->setStackColorFilter(lastColor, lastAlpha, stack, spell, false);
 		delete this;
 		return;
 	}
 
 	assert(index != 0);
 
-	const auto & prevShifter = steps[index - 1];
-	const auto & nextShifter = steps[index];
+	const auto & prevColor = effectColors[index - 1];
+	const auto & nextColor = effectColors[index];
+	const auto & prevAlpha = transparency[index - 1];
+	const auto & nextAlpha = transparency[index];
 
 	float prevPoint = timePoints[index-1];
 	float nextPoint = timePoints[index];
@@ -611,9 +614,10 @@ void ColorTransformAnimation::tick(uint32_t msPassed)
 	float stepDuration = (nextPoint - prevPoint);
 	float factor = localProgress / stepDuration;
 
-	auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor);
+	const auto & currColor = vstd::lerp(prevColor, nextColor, factor);
+	const auto & currAlpha = vstd::lerp(prevAlpha, nextAlpha, factor);
 
-	owner.stacksController->setStackColorFilter(shifter, stack, spell, true);
+	owner.stacksController->setStackColorFilter(currColor, currAlpha, stack, spell, true);
 }
 
 ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell):
@@ -622,10 +626,11 @@ ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const
 	totalProgress(0.f)
 {
 	auto effect = owner.effectsController->getMuxerEffect(colorFilterName);
-	steps = effect.filters;
+	effectColors = effect.effectColors;
+	transparency = effect.transparency;
 	timePoints = effect.timePoints;
 
-	assert(!steps.empty() && steps.size() == timePoints.size());
+	assert(!effectColors.empty() && effectColors.size() == timePoints.size());
 
 	logAnim->debug("Created ColorTransformAnimation for %s", stack->getName());
 }
diff --git a/client/battle/BattleAnimationClasses.h b/client/battle/BattleAnimationClasses.h
index 1314c2350..ff501cbfa 100644
--- a/client/battle/BattleAnimationClasses.h
+++ b/client/battle/BattleAnimationClasses.h
@@ -11,6 +11,7 @@
 
 #include "../../lib/battle/BattleHexArray.h"
 #include "../../lib/filesystem/ResourcePath.h"
+#include "../../lib/Color.h"
 #include "BattleConstants.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -113,8 +114,10 @@ public:
 
 class ColorTransformAnimation : public BattleStackAnimation
 {
-	std::vector<ColorFilter> steps;
+	std::vector<ColorRGBA> effectColors;
+	std::vector<float> transparency;
 	std::vector<float> timePoints;
+
 	const CSpell * spell;
 
 	float totalProgress;
diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp
index 0d3caf2ea..41f67e981 100644
--- a/client/battle/BattleEffectsController.cpp
+++ b/client/battle/BattleEffectsController.cpp
@@ -143,7 +143,8 @@ void BattleEffectsController::loadColorMuxers()
 		for (const JsonNode & entry : muxer.second.Vector() )
 		{
 			effect.timePoints.push_back(entry["time"].Float());
-			effect.filters.push_back(ColorFilter::genFromJson(entry));
+			effect.effectColors.push_back(ColorRGBA(255*entry["color"][0].Float(), 255*entry["color"][1].Float(), 255*entry["color"][2].Float(), 255*entry["color"][3].Float()));
+			effect.transparency.push_back(entry["alpha"].Float() * 255);
 		}
 		colorMuxerEffects[identifier] = effect;
 	}
diff --git a/client/battle/BattleEffectsController.h b/client/battle/BattleEffectsController.h
index 255313d21..5aa708dff 100644
--- a/client/battle/BattleEffectsController.h
+++ b/client/battle/BattleEffectsController.h
@@ -11,6 +11,7 @@
 
 #include "../../lib/battle/BattleHex.h"
 #include "../../lib/Point.h"
+#include "../../lib/Color.h"
 #include "../../lib/filesystem/ResourcePath.h"
 #include "BattleConstants.h"
 
@@ -21,13 +22,19 @@ struct BattleTriggerEffect;
 
 VCMI_LIB_NAMESPACE_END
 
-struct ColorMuxerEffect;
 class CAnimation;
 class Canvas;
 class BattleInterface;
 class BattleRenderer;
 class EffectAnimation;
 
+struct ColorMuxerEffect
+{
+	std::vector<ColorRGBA> effectColors;
+	std::vector<float> transparency;
+	std::vector<float> timePoints;
+};
+
 /// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,...
 struct BattleEffect
 {
diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp
index d0be307cc..5b8f3c077 100644
--- a/client/battle/BattleStacksController.cpp
+++ b/client/battle/BattleStacksController.cpp
@@ -204,8 +204,7 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 	if (!instant)
 	{
 		// immediately make stack transparent, giving correct shifter time to start
-		auto shifterFade = ColorFilter::genAlphaShifter(0);
-		setStackColorFilter(shifterFade, stack, nullptr, true);
+		setStackColorFilter(Colors::TRANSPARENCY, 0, stack, nullptr, true);
 
 		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
 		{
@@ -324,14 +323,18 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack *
 
 void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)
 {
-	ColorFilter fullFilter = ColorFilter::genEmptyShifter();
+	ColorRGBA effectColor = Colors::TRANSPARENCY;
+	uint8_t transparency = 255;
 	for(const auto & filter : stackFilterEffects)
 	{
 		if (filter.target == stack)
-			fullFilter = ColorFilter::genCombined(fullFilter, filter.effect);
+		{
+			effectColor = filter.effectColor;
+			transparency = static_cast<int>(filter.transparency) * transparency / 255;
+		}
 	}
 
-	stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit
+	stackAnimation[stack->unitId()]->nextFrame(canvas, effectColor, transparency, facingRight(stack)); // do actual blit
 }
 
 void BattleStacksController::tick(uint32_t msPassed)
@@ -769,18 +772,19 @@ Point BattleStacksController::getStackPositionAtHex(const BattleHex & hexNum, co
 	return ret;
 }
 
-void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent)
+void BattleStacksController::setStackColorFilter(const ColorRGBA & effectColor, uint8_t transparency, const CStack * target, const CSpell * source, bool persistent)
 {
 	for (auto & filter : stackFilterEffects)
 	{
 		if (filter.target == target && filter.source == source)
 		{
-			filter.effect     = effect;
+			filter.effectColor = effectColor;
+			filter.transparency	= transparency;
 			filter.persistent = persistent;
 			return;
 		}
 	}
-	stackFilterEffects.push_back({ effect, target, source, persistent });
+	stackFilterEffects.push_back({ target, source, effectColor, transparency, persistent });
 }
 
 void BattleStacksController::removeExpiredColorFilters()
@@ -791,7 +795,7 @@ void BattleStacksController::removeExpiredColorFilters()
 		{
 			if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(filter.source->id)), Selector::all))
 				return true;
-			if (filter.effect == ColorFilter::genEmptyShifter())
+			if (filter.effectColor == Colors::TRANSPARENCY && filter.transparency == 255)
 				return true;
 		}
 		return false;
diff --git a/client/battle/BattleStacksController.h b/client/battle/BattleStacksController.h
index 38019befb..c7c3f885a 100644
--- a/client/battle/BattleStacksController.h
+++ b/client/battle/BattleStacksController.h
@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../render/ColorFilter.h"
+#include "../../lib/Color.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -37,9 +37,10 @@ class IImage;
 
 struct BattleStackFilterEffect
 {
-	ColorFilter effect;
 	const CStack * target;
 	const CSpell * source;
+	ColorRGBA effectColor;
+	uint8_t transparency;
 	bool persistent;
 };
 
@@ -134,7 +135,7 @@ public:
 	/// Adds new color filter effect targeting stack
 	/// Effect will last as long as stack is affected by specified spell (unless effect is persistent)
 	/// If effect from same (target, source) already exists, it will be updated
-	void setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell *source, bool persistent);
+	void setStackColorFilter(const ColorRGBA & effect, uint8_t transparency, const CStack * target, const CSpell *source, bool persistent);
 	void addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims
 
 	const CStack* getActiveStack() const;
diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp
index 60a59cf54..c900a46b4 100644
--- a/client/battle/CreatureAnimation.cpp
+++ b/client/battle/CreatureAnimation.cpp
@@ -24,11 +24,6 @@ static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 };
 static const ColorRGBA creatureGoldBorder = { 255, 255, 0, 255 };
 static const ColorRGBA creatureNoBorder  =  { 0, 0, 0, 0 };
 
-static ColorRGBA genShadow(ui8 alpha)
-{
-	return ColorRGBA(0, 0, 0, alpha);
-}
-
 ColorRGBA AnimationControls::getBlueBorder()
 {
 	return creatureBlueBorder;
@@ -192,7 +187,6 @@ void CreatureAnimation::setType(ECreatureAnimType type)
 CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedController controller)
 	: name(name_),
 	  speed(0.1f),
-	  shadowAlpha(128),
 	  currentFrame(0),
 	  animationEnd(-1),
 	  elapsedTime(0),
@@ -324,11 +318,8 @@ static ColorRGBA genBorderColor(ui8 alpha, const ColorRGBA & base)
 	return ColorRGBA(base.r, base.g, base.b, ui8(base.a * alpha / 256));
 }
 
-void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight)
+void CreatureAnimation::nextFrame(Canvas & canvas, const ColorRGBA & effectColor, uint8_t transparency, bool facingRight)
 {
-	ColorRGBA shadowTest = shifter.shiftColor(genShadow(128));
-	shadowAlpha = shadowTest.a;
-
 	size_t frame = static_cast<size_t>(floor(currentFrame));
 
 	std::shared_ptr<IImage> image;
@@ -345,7 +336,8 @@ void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter,
 		else
 			image->setOverlayColor(Colors::TRANSPARENCY);
 
-		image->adjustPalette(shifter, 0);
+		image->setEffectColor(effectColor);
+		image->setAlpha(transparency);
 
 		canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h));
 	}
diff --git a/client/battle/CreatureAnimation.h b/client/battle/CreatureAnimation.h
index 09e3fd259..168f8d117 100644
--- a/client/battle/CreatureAnimation.h
+++ b/client/battle/CreatureAnimation.h
@@ -94,9 +94,6 @@ private:
 	///type of animation being displayed
 	ECreatureAnimType type;
 
-	/// current value of shadow transparency
-	uint8_t shadowAlpha;
-
 	/// border color, disabled if alpha = 0
 	ColorRGBA border;
 
@@ -127,7 +124,7 @@ public:
 	/// returns currently rendered type of animation
 	ECreatureAnimType getType() const;
 
-	void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight);
+	void nextFrame(Canvas & canvas, const ColorRGBA & effectColor, uint8_t transparency, bool facingRight);
 
 	/// should be called every frame, return true when animation was reset to beginning
 	bool incrementFrame(float timePassed);
diff --git a/client/render/CanvasImage.h b/client/render/CanvasImage.h
index 56452c926..154d317f3 100644
--- a/client/render/CanvasImage.h
+++ b/client/render/CanvasImage.h
@@ -32,6 +32,7 @@ public:
 	void setAlpha(uint8_t value) override{};
 	void playerColored(const PlayerColor & player) override{};
 	void setOverlayColor(const ColorRGBA & color) override{};
+	void setEffectColor(const ColorRGBA & color) override{};
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override{};
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override{};
 
diff --git a/client/render/ColorFilter.cpp b/client/render/ColorFilter.cpp
index 9e530009f..12ffb7c16 100644
--- a/client/render/ColorFilter.cpp
+++ b/client/render/ColorFilter.cpp
@@ -129,40 +129,3 @@ ColorFilter ColorFilter::genCombined(const ColorFilter & left, const ColorFilter
 	float a = left.a * right.a;
 	return genMuxerShifter(r,g,b,a);
 }
-
-ColorFilter ColorFilter::genFromJson(const JsonNode & entry)
-{
-	ChannelMuxer r{ 1.f, 0.f, 0.f, 0.f };
-	ChannelMuxer g{ 0.f, 1.f, 0.f, 0.f };
-	ChannelMuxer b{ 0.f, 0.f, 1.f, 0.f };
-	float a{ 1.0};
-
-	if (!entry["red"].isNull())
-	{
-		r.r = entry["red"].Vector()[0].Float();
-		r.g = entry["red"].Vector()[1].Float();
-		r.b = entry["red"].Vector()[2].Float();
-		r.a = entry["red"].Vector()[3].Float();
-	}
-
-	if (!entry["green"].isNull())
-	{
-		g.r = entry["green"].Vector()[0].Float();
-		g.g = entry["green"].Vector()[1].Float();
-		g.b = entry["green"].Vector()[2].Float();
-		g.a = entry["green"].Vector()[3].Float();
-	}
-
-	if (!entry["blue"].isNull())
-	{
-		b.r = entry["blue"].Vector()[0].Float();
-		b.g = entry["blue"].Vector()[1].Float();
-		b.b = entry["blue"].Vector()[2].Float();
-		b.a = entry["blue"].Vector()[3].Float();
-	}
-
-	if (!entry["alpha"].isNull())
-		a = entry["alpha"].Float();
-
-	return genMuxerShifter(r,g,b,a);
-}
diff --git a/client/render/ColorFilter.h b/client/render/ColorFilter.h
index 2a0268fe0..a2403ab52 100644
--- a/client/render/ColorFilter.h
+++ b/client/render/ColorFilter.h
@@ -54,13 +54,4 @@ public:
 
 	/// Scales down strength of a shifter to a specified factor
 	static ColorFilter genInterpolated(const ColorFilter & left, const ColorFilter & right, float power);
-
-	/// Generates object using supplied Json config
-	static ColorFilter genFromJson(const JsonNode & entry);
-};
-
-struct ColorMuxerEffect
-{
-	std::vector<ColorFilter> filters;
-	std::vector<float> timePoints;
 };
diff --git a/client/render/IImage.h b/client/render/IImage.h
index 9c2216553..cca4a8305 100644
--- a/client/render/IImage.h
+++ b/client/render/IImage.h
@@ -51,6 +51,7 @@ enum class EImageBlitMode : uint8_t
 	WITH_SHADOW_AND_FLAG_COLOR,
 
 	/// RGBA, contains only body, with shadow and overlay disabled
+	GRAYSCALE_BODY_HIDE_SELECTION,
 	ONLY_BODY_HIDE_SELECTION,
 	ONLY_BODY_HIDE_FLAG_COLOR,
 
@@ -104,8 +105,8 @@ public:
 
 	virtual void setAlpha(uint8_t value) = 0;
 
-	//only indexed bitmaps with 7 special colors
 	virtual void setOverlayColor(const ColorRGBA & color) = 0;
+	virtual void setEffectColor(const ColorRGBA & color) = 0;
 
 	virtual ~IImage() = default;
 };
diff --git a/client/renderSDL/ScalableImage.cpp b/client/renderSDL/ScalableImage.cpp
index 40a22c722..956679aa8 100644
--- a/client/renderSDL/ScalableImage.cpp
+++ b/client/renderSDL/ScalableImage.cpp
@@ -103,6 +103,10 @@ void ScalableImageParameters::preparePalette(const SDL_Palette * originalPalette
 		case EImageBlitMode::ONLY_SELECTION:
 			adjustPalette(originalPalette, blitMode, ColorFilter::genAlphaShifter(0), 0);
 			break;
+		case EImageBlitMode::GRAYSCALE_BODY_HIDE_SELECTION:
+			adjustPalette(originalPalette, blitMode, ColorFilter::genMuxerShifter( { 0.299, 0.587, 0.114, 0}, { 0.299, 0.587, 0.114, 0}, { 0.299, 0.587, 0.114, 0}, 1), 0);
+			break;
+
 	}
 
 	switch(blitMode)
@@ -120,6 +124,7 @@ void ScalableImageParameters::preparePalette(const SDL_Palette * originalPalette
 		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
 		case EImageBlitMode::ONLY_FLAG_COLOR:
 		case EImageBlitMode::ONLY_SELECTION:
+		case EImageBlitMode::GRAYSCALE_BODY_HIDE_SELECTION:
 			setShadowTransparency(originalPalette, 0.0);
 			break;
 	}
@@ -140,6 +145,7 @@ void ScalableImageParameters::preparePalette(const SDL_Palette * originalPalette
 			break;
 		case EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION:
 		case EImageBlitMode::ONLY_BODY_HIDE_SELECTION:
+		case EImageBlitMode::GRAYSCALE_BODY_HIDE_SELECTION:
 			setOverlayColor(originalPalette, Colors::TRANSPARENCY, true);
 			break;
 	}
@@ -273,11 +279,15 @@ void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Re
 	bool shadowLoading = scaled.at(scalingFactor).shadow.at(0) && scaled.at(scalingFactor).shadow.at(0)->isLoading();
 	bool bodyLoading = scaled.at(scalingFactor).body.at(0) && scaled.at(scalingFactor).body.at(0)->isLoading();
 	bool overlayLoading = scaled.at(scalingFactor).overlay.at(0) && scaled.at(scalingFactor).overlay.at(0)->isLoading();
+	bool grayscaleLoading = scaled.at(scalingFactor).bodyGrayscale.at(0) && scaled.at(scalingFactor).bodyGrayscale.at(0)->isLoading();
 	bool playerLoading = parameters.player != PlayerColor::CANNOT_DETERMINE && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum()) && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum())->isLoading();
 
-	if (shadowLoading || bodyLoading || overlayLoading || playerLoading)
+	if (shadowLoading || bodyLoading || overlayLoading || playerLoading || grayscaleLoading)
 	{
 		getFlippedImage(scaled[1].body)->scaledDraw(where, parameters.palette, dimensions() * scalingFactor, dest, src, parameters.colorMultiplier, parameters.alphaValue, locator.layer);
+
+		if (parameters.effectColorMultiplier.a != ColorRGBA::ALPHA_TRANSPARENT)
+			getFlippedImage(scaled[1].body)->scaledDraw(where, parameters.palette, dimensions() * scalingFactor, dest, src, parameters.effectColorMultiplier, parameters.alphaValue, locator.layer);
 		return;
 	}
 
@@ -292,6 +302,9 @@ void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Re
 	{
 		if (scaled.at(scalingFactor).body.at(0))
 			flipAndDraw(scaled.at(scalingFactor).body, parameters.colorMultiplier, parameters.alphaValue);
+
+		if (scaled.at(scalingFactor).bodyGrayscale.at(0) && parameters.effectColorMultiplier.a != ColorRGBA::ALPHA_TRANSPARENT)
+			flipAndDraw(scaled.at(scalingFactor).bodyGrayscale, parameters.effectColorMultiplier, parameters.alphaValue);
 	}
 
 	if (scaled.at(scalingFactor).overlay.at(0))
@@ -370,6 +383,24 @@ void ScalableImageInstance::setOverlayColor(const ColorRGBA & color)
 		parameters.setOverlayColor(image->getPalette(), color, blitMode == EImageBlitMode::WITH_SHADOW_AND_SELECTION);
 }
 
+void ScalableImageInstance::setEffectColor(const ColorRGBA & color)
+{
+	parameters.effectColorMultiplier = color;
+
+	if (parameters.palette)
+	{
+		const auto grayscaleFilter = ColorFilter::genMuxerShifter( { 0.299, 0.587, 0.114, 0}, { 0.299, 0.587, 0.114, 0}, { 0.299, 0.587, 0.114, 0}, 1);
+		const auto effectStrengthFilter = ColorFilter::genRangeShifter( 0, 0, 0, color.r / 255.f, color.g / 255.f, color.b / 255.f);
+		const auto effectFilter = ColorFilter::genCombined(grayscaleFilter, effectStrengthFilter);
+		const auto effectiveFilter = ColorFilter::genInterpolated(ColorFilter::genEmptyShifter(), effectFilter, color.a / 255.f);
+
+		parameters.adjustPalette(image->getPalette(), blitMode, effectiveFilter, 0);
+	}
+
+	if (color.a != ColorRGBA::ALPHA_TRANSPARENT)
+		image->prepareEffectImage();
+}
+
 void ScalableImageInstance::playerColored(const PlayerColor & player)
 {
 	parameters.player = player;
@@ -424,11 +455,19 @@ std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EIm
 	if (loadedImage)
 		return loadedImage;
 
+	// optional images for 1x resolution - only try load them, don't attempt to generate
+	bool optionalImage =
+		mode == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR ||
+		mode == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION ||
+		mode == EImageBlitMode::ONLY_FLAG_COLOR ||
+		mode == EImageBlitMode::ONLY_SELECTION ||
+		mode == EImageBlitMode::GRAYSCALE_BODY_HIDE_SELECTION ||
+		color != PlayerColor::CANNOT_DETERMINE;
+
 	if (scalingFactor == 1)
 	{
-		// optional images for 1x resolution - only try load them, don't attempt to generate
 		// this block should never be called for 'body' layer - that image is loaded unconditionally before construction
-		assert(mode == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR || mode == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || mode == EImageBlitMode::ONLY_FLAG_COLOR || mode == EImageBlitMode::ONLY_SELECTION || color != PlayerColor::CANNOT_DETERMINE);
+		assert(optionalImage);
 		return nullptr;
 	}
 
@@ -441,7 +480,7 @@ std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EIm
 		{
 			if (scaling == 1)
 			{
-				if (mode == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR || mode == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || mode == EImageBlitMode::ONLY_FLAG_COLOR || mode == EImageBlitMode::ONLY_SELECTION || color != PlayerColor::CANNOT_DETERMINE)
+				if (optionalImage)
 				{
 					ScalableImageParameters parameters(getPalette(), mode);
 					return loadedImage->scaleInteger(scalingFactor, parameters.palette, mode);
@@ -560,3 +599,20 @@ void ScalableImageShared::preparePlayerColoredImage(PlayerColor color)
 {
 	loadScaledImages(GH.screenHandler().getScalingFactor(), color);
 }
+
+void ScalableImageShared::prepareEffectImage()
+{
+	int scalingFactor = GH.screenHandler().getScalingFactor();
+
+	if (scaled[scalingFactor].bodyGrayscale[0] == nullptr)
+	{
+		switch(locator.layer)
+		{
+			case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+				scaled[scalingFactor].bodyGrayscale[0] = loadOrGenerateImage(EImageBlitMode::GRAYSCALE_BODY_HIDE_SELECTION, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].bodyGrayscale[0]);
+				break;
+			default:
+				break;
+		}
+	}
+}
diff --git a/client/renderSDL/ScalableImage.h b/client/renderSDL/ScalableImage.h
index 7578130c2..82a20c56d 100644
--- a/client/renderSDL/ScalableImage.h
+++ b/client/renderSDL/ScalableImage.h
@@ -26,6 +26,7 @@ struct ScalableImageParameters : boost::noncopyable
 
 	ColorRGBA colorMultiplier = Colors::WHITE_TRUE;
 	ColorRGBA ovelayColorMultiplier = Colors::WHITE_TRUE;
+	ColorRGBA effectColorMultiplier = Colors::TRANSPARENCY;
 
 	PlayerColor player = PlayerColor::CANNOT_DETERMINE;
 	uint8_t alphaValue = 255;
@@ -64,6 +65,9 @@ class ScalableImageShared final : public std::enable_shared_from_this<ScalableIm
 		/// Upscaled overlay (player color, selection highlight) of our image, may be null
 		FlippedImages overlay;
 
+		/// Upscaled grayscale version of body, for special effects in combat (e.g clone / petrify / berserk)
+		FlippedImages bodyGrayscale;
+
 		/// player-colored images of this particular scale, mostly for UI. These are never flipped in h3
 		PlayerColoredImages playerColored;
 	};
@@ -91,6 +95,7 @@ public:
 
 	std::shared_ptr<ScalableImageInstance> createImageReference();
 
+	void prepareEffectImage();
 	void preparePlayerColoredImage(PlayerColor color);
 };
 
@@ -115,6 +120,7 @@ public:
 	void setAlpha(uint8_t value) override;
 	void draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const override;
 	void setOverlayColor(const ColorRGBA & color) override;
+	void setEffectColor(const ColorRGBA & color) override;
 	void playerColored(const PlayerColor & player) override;
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
diff --git a/config/battleEffects.json b/config/battleEffects.json
index 498290cd4..6663297c2 100644
--- a/config/battleEffects.json
+++ b/config/battleEffects.json
@@ -10,12 +10,8 @@
 				// Note that actual speed of animation is subject to changes from in-game animation speed setting
 				"time" : 0.0,
 
-				// Optional. Transformation filter for red, green and blue components of a color
-				// Applies transformation with specified parameters to each channel. Formula:
-				// result = red * (value 1) + green * (value 2) + blue * (value 3) + (value 4)
-				"red"   : [ 1.0, 0.0, 0.0, 0.0 ],
-				"green" : [ 0.0, 1.0, 0.0, 0.0 ],
-				"blue"  : [ 0.0, 0.0, 1.0, 0.0 ],
+				// Optional. Adds grayscale overlay on top of creature, multiplied by specified color per channel (rgba order)
+				"color"   : [ 1.0, 0.0, 0.0, 1.0 ],
 				
 				/// Optional. Transparency filter, makes stack appear semi-transparent, used mostly for fade-in effects
 				/// Value 0 = full transparency, 1 = fully opaque
@@ -25,23 +21,21 @@
 	
 		"petrification" : [
 			{
-				"time" : 0.0
+				"time" : 0.0,
+				"alpha" : 1.0
 			},
 			{
 				"time"  : 1.0,
-				// Conversion to grayscale, using human eye perception factor for channels
-				"red"   : [ 0.299, 0.587, 0.114, 0.0 ],
-				"green" : [ 0.299, 0.587, 0.114, 0.0 ],
-				"blue"  : [ 0.299, 0.587, 0.114, 0.0 ],
+				"color"  : [ 1.0, 1.0, 1.0, 1.0 ], //grayscale
+				"alpha" : 1.0
 			}
 		],
 		"cloning" : [
 			{ 
 				// No fade in - will be handled by summonFadeIn effect
 				"time" : 0.0, 
-				"red"   : [ 0.5, 0.0, 0.0, 0.0 ],
-				"green" : [ 0.0, 0.5, 0.0, 0.0 ],
-				"blue"  : [ 0.0, 0.0, 0.5, 0.5 ],
+				"color"  : [ 0.0, 0.0, 1.0, 1.0 ], //blue
+				"alpha" : 1.0
 			}
 		],
 		"summonFadeIn" : [
@@ -50,12 +44,14 @@
 				"alpha" : 0.0
 			},
 			{
-				"time" : 1.0
+				"time" : 1.0,
+				"alpha" : 1.0
 			},
 		],
 		"summonFadeOut" : [
 			{
-				"time" : 0.0
+				"time" : 0.0,
+				"alpha" : 1.0
 			},
 			{
 				"time" : 1.0,
@@ -68,12 +64,14 @@
 				"alpha" : 0.0
 			},
 			{
-				"time" : 0.5
+				"time" : 0.5,
+				"alpha" : 1.0
 			},
 		],
 		"teleportFadeOut" : [
 			{
-				"time" : 0.0
+				"time" : 0.0,
+				"alpha" : 1.0
 			},
 			{
 				"time" : 0.5,
@@ -82,31 +80,17 @@
 		],
 		"bloodlust" : [
 			{
-				"time" : 0.0
-			},
-			{
-				"time"  : 0.25, 
-				"red"   : [ 0.5, 0.0, 0.5, 0.4 ],
-				"green" : [ 0.0, 1.0, 0.0, 0.0 ],
-				"blue"  : [ 0.0, 0.0, 1.0, 0.0 ],
+				"time" : 0.0,
 				"alpha" : 1.0
 			},
 			{
-				"time"  : 0.5, 
-				"red"   : [ 0.6, 0.6, 0.6, 0.0 ],
-				"green" : [ 0.0, 0.5, 0.0, 0.0 ],
-				"blue"  : [ 0.0, 0.0, 0.5, 0.0 ],
-				"alpha" : 1.0
-			},
-			{
-				"time"  : 0.75, 
-				"red"   : [ 0.5, 0.0, 0.5, 0.4 ],
-				"green" : [ 0.0, 1.0, 0.0, 0.0 ],
-				"blue"  : [ 0.0, 0.0, 1.0, 0.0 ],
+				"time"  : 0.5,
+				"color"  : [ 1.0, 0.0, 0.0, 1.0 ], //red
 				"alpha" : 1.0
 			},
 			{
 				"time" : 1.0,
+				"alpha" : 1.0
 			},
 		],
 	}
diff --git a/lib/Color.h b/lib/Color.h
index 1bc03414a..a983df691 100644
--- a/lib/Color.h
+++ b/lib/Color.h
@@ -64,4 +64,18 @@ public:
 	}
 };
 
+namespace vstd
+{
+template<typename Floating>
+ColorRGBA lerp(const ColorRGBA & left, const ColorRGBA & right, const Floating & factor)
+{
+	return ColorRGBA(
+		vstd::lerp(left.r, right.r, factor),
+		vstd::lerp(left.g, right.g, factor),
+		vstd::lerp(left.b, right.b, factor),
+		vstd::lerp(left.a, right.a, factor)
+	);
+}
+}
+
 VCMI_LIB_NAMESPACE_END