浏览代码

Implemented Bloodlust & Petrification effect

- ColorFilter is now in separate file
- Moved lerp function into global.h
- Bloodlust visuals mostly matches H3
- Petrify visual matches H3
- TODO: Adjust timing of all ColorFilter efects to match H3
- TODO: Petrify should pause stack animations
- TODO: ColorFilter-powered effects should be configurable in Spell system
Ivan Savenko 2 年之前
父节点
当前提交
864990db13

+ 7 - 0
Global.h

@@ -760,6 +760,13 @@ namespace vstd
 		return v;
 	}
 
+	//c++20 feature
+	template<typename Arithmetic, typename Floating>
+	Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f)
+	{
+		return a + (b - a) * f;
+	}
+
 	using boost::math::round;
 }
 using vstd::operator-=;

+ 2 - 0
client/CMakeLists.txt

@@ -21,6 +21,7 @@ set(client_SRCS
 		gui/CCursorHandler.cpp
 		gui/CGuiHandler.cpp
 		gui/CIntObject.cpp
+		gui/ColorFilter.cpp
 		gui/Fonts.cpp
 		gui/Geometries.cpp
 		gui/SDL_Extensions.cpp
@@ -104,6 +105,7 @@ set(client_HEADERS
 		gui/Canvas.h
 		gui/CCursorHandler.h
 		gui/CGuiHandler.h
+		gui/ColorFilter.h
 		gui/CIntObject.h
 		gui/Fonts.h
 		gui/Geometries.h

+ 1 - 1
client/CreatureCostBox.cpp

@@ -18,7 +18,7 @@ CreatureCostBox::CreatureCostBox(Rect position, std::string titleText)
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
 	type |= REDRAW_PARENT;
-	pos = position + pos;
+	pos = position + pos.topLeft();
 
 	title = std::make_shared<CLabel>(pos.w/2, 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, titleText);
 }

+ 93 - 28
client/battle/BattleAnimationClasses.cpp

@@ -24,6 +24,7 @@
 #include "../CPlayerInterface.h"
 #include "../gui/CCursorHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/SDL_Extensions.h"
 
 #include "../../CCallback.h"
 #include "../../lib/CStack.h"
@@ -409,7 +410,7 @@ void MovementAnimation::nextFrame()
 	{
 		// Sets the position of the creature animation sprites
 		Point coords = owner.stacksController->getStackPositionAtHex(currentHex, stack);
-		myAnim->pos = coords;
+		myAnim->pos.moveTo(coords);
 
 		// true if creature haven't reached the final destination hex
 		if ((curentMoveIndex + 1) < destTiles.size())
@@ -431,7 +432,7 @@ MovementAnimation::~MovementAnimation()
 {
 	assert(stack);
 
-	myAnim->pos = owner.stacksController->getStackPositionAtHex(currentHex, stack);
+	myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(currentHex, stack));
 
 	if(owner.moveSoundHander != -1)
 	{
@@ -560,7 +561,7 @@ void BattleStackAnimation::rotateStack(BattleHex hex)
 {
 	setStackFacingRight(stack, !stackFacingRight(stack));
 
-	stackAnimation(stack)->pos = owner.stacksController->getStackPositionAtHex(hex, stack);
+	stackAnimation(stack)->pos.moveTo(owner.stacksController->getStackPositionAtHex(hex, stack));
 }
 
 void ReverseAnimation::setupSecondPart()
@@ -607,41 +608,109 @@ ResurrectionAnimation::ResurrectionAnimation(BattleInterface & owner, const CSta
 
 }
 
-bool FadingAnimation::init()
+bool ColorTransformAnimation::init()
 {
-	logAnim->info("FadingAnimation::init: stack %s", stack->getName());
-	//TODO: pause animation?
 	return true;
 }
 
-void FadingAnimation::nextFrame()
+void ColorTransformAnimation::nextFrame()
 {
 	float elapsed  = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
 	float fullTime = AnimationControls::getFadeInDuration();
 	float delta    = elapsed / fullTime;
-	progress += delta;
+	totalProgress += delta;
 
-	if (progress > 1.0f)
-		progress = 1.0f;
+	size_t index = 0;
 
-	uint8_t factor = stack->cloned ? 128 : 255;
-	uint8_t blue   = stack->cloned ? 128 : 0;
-	uint8_t alpha  = CSDL_Ext::lerp(from, dest, progress);
+	while (index < timePoints.size() && timePoints[index] < totalProgress )
+		++index;
 
-	ColorShifterRange shifterFade ({0, 0, blue, 0}, {factor, factor, 255, alpha});
-	stackAnimation(stack)->shiftColor(&shifterFade);
-
-	if (progress == 1.0f)
+	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);
 		delete this;
+		return;
+	}
+
+	assert(index != 0);
+
+	const auto & prevShifter = steps[index - 1];
+	const auto & nextShifter = steps[index];
+
+	float prevPoint = timePoints[index-1];
+	float nextPoint = timePoints[index];
+	float localProgress = totalProgress - prevPoint;
+	float stepDuration = (nextPoint - prevPoint);
+	float factor = localProgress / stepDuration;
+
+	auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor);
+
+	owner.stacksController->setStackColorFilter(shifter, stack, spell, true);
 }
 
-FadingAnimation::FadingAnimation(BattleInterface & owner, const CStack * _stack, uint8_t alphaFrom, uint8_t alphaDest):
+ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const CSpell * spell):
 	BattleStackAnimation(owner, _stack),
-	from(alphaFrom),
-	dest(alphaDest)
+	spell(spell),
+	totalProgress(0.f)
+{
+
+}
+
+ColorTransformAnimation * ColorTransformAnimation::bloodlustAnimation(BattleInterface & owner, const CStack * stack, const CSpell * spell)
+{
+	auto result = new ColorTransformAnimation(owner, stack, spell);
+
+	result->steps.push_back(ColorFilter::genEmptyShifter());
+	result->steps.push_back(ColorFilter::genMuxerShifter( { 0.3, 0.0, 0.3, 0.4}, { 0, 1, 0, 0}, { 0, 0, 1, 0}, 1.f ));
+	result->steps.push_back(ColorFilter::genMuxerShifter( { 0.3, 0.3, 0.3, 0.1}, { 0, 0.5, 0, 0}, { 0, 0.5, 0, 0}, 1.f ));
+	result->steps.push_back(ColorFilter::genMuxerShifter( { 0.3, 0.0, 0.3, 0.4}, { 0, 1, 0, 0}, { 0, 0, 1, 0}, 1.f ));
+	result->steps.push_back(ColorFilter::genEmptyShifter());
+
+	result->timePoints.push_back(0.0f);
+	result->timePoints.push_back(0.2f);
+	result->timePoints.push_back(0.4f);
+	result->timePoints.push_back(0.6f);
+	result->timePoints.push_back(0.8f);
+
+	return result;
+}
+
+ColorTransformAnimation * ColorTransformAnimation::cloneAnimation(BattleInterface & owner, const CStack * stack, const CSpell * spell)
+{
+	auto result = new ColorTransformAnimation(owner, stack, spell);
+	result->steps.push_back(ColorFilter::genRangeShifter( 0, 0, 0.5, 0.5, 0.5, 1.0));
+	result->timePoints.push_back(0.f);
+	return result;
+}
+
+ColorTransformAnimation * ColorTransformAnimation::petrifyAnimation(BattleInterface & owner, const CStack * stack, const CSpell * spell)
 {
+	auto result = new ColorTransformAnimation(owner, stack, spell);
+	result->steps.push_back(ColorFilter::genEmptyShifter());
+	result->steps.push_back(ColorFilter::genGrayscaleShifter());
+	result->timePoints.push_back(0.f);
+	result->timePoints.push_back(1.f);
+	return result;
 }
 
+ColorTransformAnimation * ColorTransformAnimation::fadeInAnimation(BattleInterface & owner, const CStack * stack)
+{
+	auto result = new ColorTransformAnimation(owner, stack, nullptr);
+	result->steps.push_back(ColorFilter::genAlphaShifter(0.f));
+	result->steps.push_back(ColorFilter::genEmptyShifter());
+	result->timePoints.push_back(0.f);
+	result->timePoints.push_back(1.f);
+	return result;
+}
+
+ColorTransformAnimation * ColorTransformAnimation::fadeOutAnimation(BattleInterface & owner, const CStack * stack)
+{
+	auto result = fadeInAnimation(owner, stack);
+	std::swap(result->steps[0], result->steps[1]);
+	return result;
+}
 
 RangedAttackAnimation::RangedAttackAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest_, const CStack * defender)
 	: AttackAnimation(owner_, attacker, dest_, defender),
@@ -735,11 +804,8 @@ void RangedAttackAnimation::nextFrame()
 	// animation should be paused if there is an active projectile
 	if (projectileEmitted)
 	{
-		if (owner.projectilesController->hasActiveProjectile(attackingStack))
-			stackAnimation(attackingStack)->pause();
-		else
+		if (!owner.projectilesController->hasActiveProjectile(attackingStack, false))
 		{
-			stackAnimation(attackingStack)->play();
 			if(owner.getAnimationCondition(EAnimationEvents::HIT) == false)
 				owner.setAnimationCondition(EAnimationEvents::HIT, true);
 		}
@@ -753,7 +819,6 @@ void RangedAttackAnimation::nextFrame()
 		if ( stackAnimation(attackingStack)->getCurrentFrame() >= getAttackClimaxFrame() )
 		{
 			emitProjectile();
-			stackAnimation(attackingStack)->pause();
 			return;
 		}
 	}
@@ -761,7 +826,7 @@ void RangedAttackAnimation::nextFrame()
 
 RangedAttackAnimation::~RangedAttackAnimation()
 {
-	assert(!owner.projectilesController->hasActiveProjectile(attackingStack));
+	assert(!owner.projectilesController->hasActiveProjectile(attackingStack, false));
 	assert(projectileEmitted);
 
 	// FIXME: is this possible? Animation is over but we're yet to fire projectile?
@@ -822,7 +887,7 @@ void CatapultAnimation::nextFrame()
 	if ( !projectileEmitted)
 		return;
 
-	if (owner.projectilesController->hasActiveProjectile(attackingStack))
+	if (owner.projectilesController->hasActiveProjectile(attackingStack, false))
 		return;
 
 	explosionEmitted = true;
@@ -1171,7 +1236,7 @@ void HeroCastAnimation::nextFrame()
 		return;
 	}
 
-	if (!owner.projectilesController->hasActiveProjectile(nullptr))
+	if (!owner.projectilesController->hasActiveProjectile(nullptr, false))
 	{
 		emitAnimationEvent();
 		//TODO: check H3 - it is possible that hero animation should be paused until hit effect is over, not just projectile

+ 18 - 7
client/battle/BattleAnimationClasses.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "../../lib/battle/BattleHex.h"
+#include "../gui/Geometries.h"
 #include "BattleConstants.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -20,6 +21,8 @@ class CSpell;
 
 VCMI_LIB_NAMESPACE_END
 
+struct SDL_Color;
+class ColorFilter;
 class BattleHero;
 class CAnimation;
 class BattleInterface;
@@ -213,17 +216,25 @@ public:
 	ResurrectionAnimation(BattleInterface & owner, const CStack * _stack);
 };
 
-/// Performs fade-in or fade-out animation on stack
-class FadingAnimation : public BattleStackAnimation
+class ColorTransformAnimation : public BattleStackAnimation
 {
-	float progress;
-	uint8_t from;
-	uint8_t dest;
-public:
+	std::vector<ColorFilter> steps;
+	std::vector<float> timePoints;
+	const CSpell * spell;
+
+	float totalProgress;
+
 	bool init() override;
 	void nextFrame() override;
 
-	FadingAnimation(BattleInterface & owner, const CStack * _stack, uint8_t alphaFrom, uint8_t alphaDest);
+	ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const CSpell * spell);
+public:
+
+	static ColorTransformAnimation * petrifyAnimation  (BattleInterface & owner, const CStack * _stack, const CSpell * spell);
+	static ColorTransformAnimation * cloneAnimation    (BattleInterface & owner, const CStack * _stack, const CSpell * spell);
+	static ColorTransformAnimation * bloodlustAnimation(BattleInterface & owner, const CStack * _stack, const CSpell * spell);
+	static ColorTransformAnimation * fadeInAnimation   (BattleInterface & owner, const CStack * _stack);
+	static ColorTransformAnimation * fadeOutAnimation  (BattleInterface & owner, const CStack * _stack);
 };
 
 class RangedAttackAnimation : public AttackAnimation

+ 8 - 1
client/battle/BattleInterface.cpp

@@ -457,6 +457,7 @@ void BattleInterface::gateStateChanged(const EGateState state)
 void BattleInterface::battleFinished(const BattleResult& br)
 {
 	bresult = &br;
+	assert(getAnimationCondition(EAnimationEvents::ACTION) == false);
 	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 	stacksController->setActiveStack(nullptr);
 	displayBattleFinished();
@@ -537,7 +538,12 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 		if(stack)
 		{
 			executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
-				displaySpellEffect(spellID, stack->getPosition());
+				if (spellID == SpellID::BLOODLUST)
+					stacksController->addNewAnim( ColorTransformAnimation::bloodlustAnimation(*this, stack, spell));
+				else if (spellID == SpellID::STONE_GAZE)
+					stacksController->addNewAnim( ColorTransformAnimation::petrifyAnimation(*this, stack, spell));
+				else
+					displaySpellEffect(spellID, stack->getPosition());
 			});
 		}
 	}
@@ -794,6 +800,7 @@ void BattleInterface::tacticNextStack(const CStack * current)
 		current = stacksController->getActiveStack();
 
 	//no switching stacks when the current one is moving
+	assert(getAnimationCondition(EAnimationEvents::ACTION) == false);
 	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
 	TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE);

+ 7 - 7
client/battle/BattleProjectileController.cpp

@@ -56,8 +56,8 @@ void ProjectileMissile::show(Canvas & canvas)
 		float progress = float(step) / steps;
 
 		Point pos {
-			CSDL_Ext::lerp(from.x, dest.x, progress) - image->width() / 2,
-			CSDL_Ext::lerp(from.y, dest.y, progress) - image->height() / 2,
+			vstd::lerp(from.x, dest.x, progress) - image->width() / 2,
+			vstd::lerp(from.y, dest.y, progress) - image->height() / 2,
 		};
 
 		canvas.draw(image, pos);
@@ -84,7 +84,7 @@ void ProjectileCatapult::show(Canvas & canvas)
 	{
 		float progress = float(step) / steps;
 
-		int posX = CSDL_Ext::lerp(from.x, dest.x, progress);
+		int posX = vstd::lerp(from.x, dest.x, progress);
 		int posY = calculateCatapultParabolaY(from, dest, posX);
 		Point pos(posX, posY);
 
@@ -100,8 +100,8 @@ void ProjectileRay::show(Canvas & canvas)
 	float progress = float(step) / steps;
 
 	Point curr {
-		CSDL_Ext::lerp(from.x, dest.x, progress),
-		CSDL_Ext::lerp(from.y, dest.y, progress),
+		vstd::lerp(from.x, dest.x, progress),
+		vstd::lerp(from.y, dest.y, progress),
 	};
 
 	Point length = curr - from;
@@ -235,13 +235,13 @@ void BattleProjectileController::showProjectiles(Canvas & canvas)
 	});
 }
 
-bool BattleProjectileController::hasActiveProjectile(const CStack * stack) const
+bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const
 {
 	int stackID = stack ? stack->ID : -1;
 
 	for(auto const & instance : projectiles)
 	{
-		if(instance->shooterID == stackID)
+		if(instance->shooterID == stackID && (instance->playing || !emittedOnly))
 		{
 			return true;
 		}

+ 1 - 1
client/battle/BattleProjectileController.h

@@ -106,7 +106,7 @@ public:
 	void showProjectiles(Canvas & canvas);
 
 	/// returns true if stack has projectile that is yet to hit target
-	bool hasActiveProjectile(const CStack * stack) const;
+	bool hasActiveProjectile(const CStack * stack, bool emittedOnly) const;
 
 	/// starts rendering previously created projectile
 	void emitStackProjectile(const CStack * stack);

+ 0 - 2
client/battle/BattleSiegeController.cpp

@@ -327,9 +327,7 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
 
 void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
 {
-	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
 	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
 	if (ca.attacker != -1)
 	{

+ 86 - 41
client/battle/BattleStacksController.cpp

@@ -76,15 +76,21 @@ BattleStacksController::BattleStacksController(BattleInterface & owner):
 	amountNegative   = IImage::createFromFile("CMNUMWIN.BMP");
 	amountEffNeutral = IImage::createFromFile("CMNUMWIN.BMP");
 
-	static const ColorShifterRangeExcept shifterNormal  ({0,0,0,0}, {150,  50, 255, 255}, {255, 231, 132, 255});
-	static const ColorShifterRangeExcept shifterPositive({0,0,0,0}, { 50, 255,  50, 255}, {255, 231, 132, 255});
-	static const ColorShifterRangeExcept shifterNegative({0,0,0,0}, {255,  50,  50, 255}, {255, 231, 132, 255});
-	static const ColorShifterRangeExcept shifterNeutral ({0,0,0,0}, {255, 255,  50, 255}, {255, 231, 132, 255});
-
-	amountNormal->adjustPalette(&shifterNormal);
-	amountPositive->adjustPalette(&shifterPositive);
-	amountNegative->adjustPalette(&shifterNegative);
-	amountEffNeutral->adjustPalette(&shifterNeutral);
+	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0,0,0, 0.6, 0.2, 1.0 );
+	static const auto shifterPositive = ColorFilter::genRangeShifter( 0,0,0, 0.2, 1.0, 0.2 );
+	static const auto shifterNegative = ColorFilter::genRangeShifter( 0,0,0, 1.0, 0.2, 0.2 );
+	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0,0,0, 1.0, 1.0, 0.2 );
+
+	amountNormal->adjustPalette(shifterNormal);
+	amountPositive->adjustPalette(shifterPositive);
+	amountNegative->adjustPalette(shifterNegative);
+	amountEffNeutral->adjustPalette(shifterNeutral);
+
+	//Restore border color {255, 231, 132, 255} to its original state
+	amountNormal->resetPalette(26);
+	amountPositive->resetPalette(26);
+	amountNegative->resetPalette(26);
+	amountEffNeutral->resetPalette(26);
 
 	std::vector<const CStack*> stacks = owner.curInt->cb->battleGetAllStacks(true);
 	for(const CStack * s : stacks)
@@ -98,7 +104,7 @@ BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack)
 	if ( !stackAnimation.at(stack->ID)->isMoving())
 		return stack->getPosition();
 
-	if (stack->hasBonusOfType(Bonus::FLYING))
+	if (stack->hasBonusOfType(Bonus::FLYING) && stackAnimation.at(stack->ID)->getType() == ECreatureAnimType::MOVING )
 		return BattleHex::HEX_AFTER_ALL;
 
 	for (auto & anim : currentAnimations)
@@ -147,9 +153,7 @@ void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
 
 void BattleStacksController::stackReset(const CStack * stack)
 {
-	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
-	//assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
-	//owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
 
 	//reset orientation?
 	//stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER;
@@ -171,13 +175,6 @@ void BattleStacksController::stackReset(const CStack * stack)
 			addNewAnim(new ResurrectionAnimation(owner, stack));
 		});
 	}
-
-	//static const ColorShifterMultiplyAndAdd shifterClone ({255, 255, 0, 255}, {0, 0, 255, 0});
-	//if (stack->isClone())
-	//{
-	//	animation->shiftColor(&shifterClone);
-	//}
-	//owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 void BattleStacksController::stackAdded(const CStack * stack, bool instant)
@@ -213,12 +210,15 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 
 	if (!instant)
 	{
-		ColorShifterRange shifterFade ({0, 0, 0, 0}, {255, 255, 255, 0});
-		stackAnimation[stack->ID]->shiftColor(&shifterFade);
+		// immediately make stack transparent, giving correct shifter time to start
+		auto shifterFade = ColorFilter::genAlphaShifter(0);
+		setStackColorFilter(shifterFade, stack, nullptr, true);
 
 		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
 		{
-			addNewAnim(new FadingAnimation(owner, stack, 0, 255));
+			addNewAnim(ColorTransformAnimation::fadeInAnimation(owner, stack));
+			if (stack->isClone())
+				addNewAnim(ColorTransformAnimation::cloneAnimation(owner, stack, SpellID(SpellID::CLONE).toSpell()));
 		});
 	}
 }
@@ -353,7 +353,23 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack *
 
 void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)
 {
-	stackAnimation[stack->ID]->nextFrame(canvas, facingRight(stack)); // do actual blit
+	ColorFilter fullFilter = ColorFilter::genEmptyShifter();
+	for (auto const & filter : stackFilterEffects)
+	{
+		if (filter.target == stack)
+			fullFilter = ColorFilter::genCombined(fullFilter, filter.effect);
+	}
+
+	bool stackHasProjectile = owner.projectilesController->hasActiveProjectile(stack, true);
+	//bool stackPetrified = stack->hasBonus(Selector::source(Bonus::SPELL_EFFECT, SpellID::STONE_GAZE));
+	//bool stackFrozen = stackHasProjectile || stackPetrified;
+
+	if (stackHasProjectile)
+		stackAnimation[stack->ID]->pause();
+	else
+		stackAnimation[stack->ID]->play();
+
+	stackAnimation[stack->ID]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit
 	stackAnimation[stack->ID]->incrementFrame(float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000);
 }
 
@@ -387,14 +403,6 @@ void BattleStacksController::addNewAnim(BattleAnimation *anim)
 	owner.setAnimationCondition(EAnimationEvents::ACTION, true);
 }
 
-void BattleStacksController::stackActivated(const CStack *stack) //TODO: check it all before game state is changed due to abilities
-{
-	stackToActivate = stack;
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
-	if (stackToActivate) //during waiting stack may have gotten activated through show
-		owner.activateStack();
-}
-
 void BattleStacksController::stackRemoved(uint32_t stackID)
 {
 	if (getActiveStack() && getActiveStack()->ID == stackID)
@@ -410,6 +418,11 @@ void BattleStacksController::stackRemoved(uint32_t stackID)
 
 void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
 {
+	owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+		// remove any potentially erased petrification effect
+		removeExpiredColorFilters();
+	});
+
 	for(auto & attackedInfo : attackedInfos)
 	{
 		if (!attackedInfo.attacker)
@@ -466,9 +479,10 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 			});
 		}
 
-		if (attackedInfo.cloneKilled)
+		if (attackedInfo.killed && attackedInfo.defender->summoned)
 		{
 			owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+				addNewAnim(ColorTransformAnimation::fadeOutAnimation(owner, attackedInfo.defender));
 				stackRemoved(attackedInfo.defender->ID);
 			});
 		}
@@ -479,8 +493,6 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance)
 {
 	assert(destHex.size() > 0);
-
-	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
 	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
 
 	if(shouldRotate(stack, stack->getPosition(), destHex[0]))
@@ -498,7 +510,6 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleH
 
 void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 {
-	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
 	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
 
 	bool needsReverse =
@@ -566,7 +577,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 		}
 	});
 
-	if (info.spellEffect)
+	if (info.spellEffect != SpellID::NONE)
 	{
 		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
 		{
@@ -626,9 +637,7 @@ bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex
 
 void BattleStacksController::endAction(const BattleAction* action)
 {
-	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
 	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
 	//check if we should reverse stacks
 	TStacks stacks = owner.curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY);
@@ -644,7 +653,7 @@ void BattleStacksController::endAction(const BattleAction* action)
 	}
 	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
-	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
+	//Ensure that all animation flags were reset
 	assert(owner.getAnimationCondition(EAnimationEvents::OPENING) == false);
 	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
 	assert(owner.getAnimationCondition(EAnimationEvents::MOVEMENT) == false);
@@ -653,14 +662,23 @@ void BattleStacksController::endAction(const BattleAction* action)
 	assert(owner.getAnimationCondition(EAnimationEvents::PROJECTILES) == false);
 
 	owner.controlPanel->blockUI(activeStack == nullptr);
+	removeExpiredColorFilters();
 }
 
 void BattleStacksController::startAction(const BattleAction* action)
 {
 	setHoveredStack(nullptr);
+	removeExpiredColorFilters();
+}
+
+void BattleStacksController::stackActivated(const CStack *stack) //TODO: check it all before game state is changed due to abilities
+{
+	stackToActivate = stack;
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.activateStack();
 }
 
-void BattleStacksController::activateStack()
+void BattleStacksController::activateStack() //TODO: check it all before game state is changed due to abilities
 {
 	if ( !currentAnimations.empty())
 		return;
@@ -765,5 +783,32 @@ Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CSta
 	}
 	//returning
 	return ret + owner.pos.topLeft();
+}
+
+void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent)
+{
+	for (auto & filter : stackFilterEffects)
+	{
+		if (filter.target == target && filter.source == source)
+		{
+			filter.effect     = effect;
+			filter.persistent = persistent;
+			return;
+		}
+	}
+	stackFilterEffects.push_back({ effect, target, source, persistent });
+}
 
+void BattleStacksController::removeExpiredColorFilters()
+{
+	vstd::erase_if(stackFilterEffects, [&](const BattleStackFilterEffect & filter)
+	{
+		if (filter.persistent)
+			return false;
+		if (filter.effect == ColorFilter::genEmptyShifter())
+			return false;
+		if (filter.target->hasBonus(Selector::source(Bonus::SPELL_EFFECT, filter.source->id)))
+			return false;
+		return true;
+	});
 }

+ 20 - 1
client/battle/BattleStacksController.h

@@ -10,12 +10,14 @@
 #pragma once
 
 #include "../gui/Geometries.h"
+#include "../gui/ColorFilter.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
 struct BattleHex;
 class BattleAction;
 class CStack;
+class CSpell;
 class SpellID;
 
 VCMI_LIB_NAMESPACE_END
@@ -23,6 +25,7 @@ VCMI_LIB_NAMESPACE_END
 struct StackAttackedInfo;
 struct StackAttackInfo;
 
+class ColorFilter;
 class Canvas;
 class BattleInterface;
 class BattleAnimation;
@@ -31,6 +34,14 @@ class BattleAnimation;
 class BattleRenderer;
 class IImage;
 
+struct BattleStackFilterEffect
+{
+	ColorFilter effect;
+	const CStack * target;
+	const CSpell * source;
+	bool persistent;
+};
+
 /// Class responsible for handling stacks in battle
 /// Handles ordering of stacks animation
 /// As well as rendering of stacks, their amount boxes
@@ -47,13 +58,16 @@ class BattleStacksController
 	/// currently displayed animations <anim, initialized>
 	std::vector<BattleAnimation *> currentAnimations;
 
+	/// currently active color effects on stacks, in order of their addition (which corresponds to their apply order)
+	std::vector<BattleStackFilterEffect> stackFilterEffects;
+
 	/// animations of creatures from fighting armies (order by BattleInfo's stacks' ID)
 	std::map<int32_t, std::shared_ptr<CreatureAnimation>> stackAnimation;
 
 	/// <creatureID, if false reverse creature's animation> //TODO: move it to battle callback
 	std::map<int, bool> stackFacingRight;
 
-	/// number of active stack; nullptr - no one
+	/// currently active stack; nullptr - no one
 	const CStack *activeStack;
 
 	/// stack below mouse pointer, used for border animation
@@ -79,6 +93,7 @@ class BattleStacksController
 	std::shared_ptr<IImage> getStackAmountBox(const CStack * stack);
 
 	void executeAttackAnimations();
+	void removeExpiredColorFilters();
 public:
 	BattleStacksController(BattleInterface & owner);
 
@@ -110,6 +125,10 @@ public:
 
 	void collectRenderableObjects(BattleRenderer & renderer);
 
+	/// 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 addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims
 	void updateBattleAnimations();
 

+ 6 - 14
client/battle/CreatureAnimation.cpp

@@ -14,6 +14,7 @@
 #include "../../lib/CCreatureHandler.h"
 
 #include "../gui/Canvas.h"
+#include "../gui/ColorFilter.h"
 
 static const SDL_Color creatureBlueBorder = { 0, 255, 255, 255 };
 static const SDL_Color creatureGoldBorder = { 255, 255, 0, 255 };
@@ -158,19 +159,6 @@ void CreatureAnimation::setType(ECreatureAnimType::Type type)
 	play();
 }
 
-void CreatureAnimation::shiftColor(const ColorShifter* shifter)
-{
-	SDL_Color shadowTest = shifter->shiftColor(genShadow(128));
-
-	shadowAlpha = shadowTest.a;
-
-	if(forward)
-		forward->shiftColor(shifter);
-
-	if(reverse)
-		reverse->shiftColor(shifter);
-}
-
 CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController controller)
 	: name(name_),
 	  speed(0.1f),
@@ -327,8 +315,11 @@ void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target)
 	target[6] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border));
 }
 
-void CreatureAnimation::nextFrame(Canvas & canvas, bool facingRight)
+void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight)
 {
+	SDL_Color shadowTest = shifter.shiftColor(genShadow(128));
+	shadowAlpha = shadowTest.a;
+
 	size_t frame = static_cast<size_t>(floor(currentFrame));
 
 	std::shared_ptr<IImage> image;
@@ -344,6 +335,7 @@ void CreatureAnimation::nextFrame(Canvas & canvas, bool facingRight)
 		genSpecialPalette(SpecialPalette);
 
 		image->setSpecialPallete(SpecialPalette);
+		image->adjustPalette(shifter);
 
 		canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h));
 

+ 1 - 4
client/battle/CreatureAnimation.h

@@ -118,16 +118,13 @@ public:
 	/// returns currently rendered type of animation
 	ECreatureAnimType::Type getType() const;
 
-	void nextFrame(Canvas & canvas, bool facingRight);
+	void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight);
 
 	/// should be called every frame, return true when animation was reset to beginning
 	bool incrementFrame(float timePassed);
 
 	void setBorderColor(SDL_Color palette);
 
-	/// apply color tint effect
-	void shiftColor(const ColorShifter * shifter);
-
 	/// Gets the current frame ID within current group.
 	float getCurrentFrame() const;
 

+ 15 - 4
client/gui/CAnimation.cpp

@@ -12,6 +12,7 @@
 
 #include "SDL_Extensions.h"
 #include "SDL_Pixels.h"
+#include "ColorFilter.h"
 
 #include "../CBitmapHandler.h"
 #include "../Graphics.h"
@@ -105,7 +106,8 @@ public:
 	void verticalFlip() override;
 
 	void shiftPalette(int from, int howMany) override;
-	void adjustPalette(const ColorShifter * shifter) override;
+	void adjustPalette(const ColorFilter & shifter) override;
+	void resetPalette(int colorID) override;
 	void resetPalette() override;
 
 	void setSpecialPallete(const SpecialPalette & SpecialPalette) override;
@@ -789,7 +791,7 @@ void SDLImage::shiftPalette(int from, int howMany)
 	}
 }
 
-void SDLImage::adjustPalette(const ColorShifter * shifter)
+void SDLImage::adjustPalette(const ColorFilter & shifter)
 {
 	if(originalPalette == nullptr)
 		return;
@@ -799,7 +801,7 @@ void SDLImage::adjustPalette(const ColorShifter * shifter)
 	// Note: here we skip the first 8 colors in the palette that predefined in H3Palette
 	for(int i = 8; i < palette->ncolors; i++)
 	{
-		palette->colors[i] = shifter->shiftColor(originalPalette->colors[i]);
+		palette->colors[i] = shifter.shiftColor(originalPalette->colors[i]);
 	}
 }
 
@@ -812,6 +814,15 @@ void SDLImage::resetPalette()
 	SDL_SetPaletteColors(surf->format->palette, originalPalette->colors, 0, originalPalette->ncolors);
 }
 
+void SDLImage::resetPalette( int colorID )
+{
+	if(originalPalette == nullptr)
+		return;
+
+	// Always keept the original palette not changed, copy a new palette to assign to surface
+	SDL_SetPaletteColors(surf->format->palette, originalPalette->colors + colorID, colorID, 1);
+}
+
 void SDLImage::setSpecialPallete(const IImage::SpecialPalette & SpecialPalette)
 {
 	if(surf->format->palette)
@@ -1079,7 +1090,7 @@ void CAnimation::duplicateImage(const size_t sourceGroup, const size_t sourceFra
 		load(index, targetGroup);
 }
 
-void CAnimation::shiftColor(const ColorShifter * shifter)
+void CAnimation::shiftColor(const ColorFilter & shifter)
 {
 	for(auto groupIter = images.begin(); groupIter != images.end(); groupIter++)
 	{

+ 4 - 3
client/gui/CAnimation.h

@@ -29,7 +29,7 @@ VCMI_LIB_NAMESPACE_END
 
 struct SDL_Surface;
 class CDefFile;
-class ColorShifter;
+class ColorFilter;
 
 /*
  * Base class for images, can be used for non-animation pictures as well
@@ -62,7 +62,8 @@ public:
 
 	//only indexed bitmaps, 16 colors maximum
 	virtual void shiftPalette(int from, int howMany) = 0;
-	virtual void adjustPalette(const ColorShifter * shifter) = 0;
+	virtual void adjustPalette(const ColorFilter & shifter) = 0;
+	virtual void resetPalette(int colorID) = 0;
 	virtual void resetPalette() = 0;
 
 	//only indexed bitmaps with 7 special colors
@@ -122,7 +123,7 @@ public:
 	void duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup);
 
 	// adjust the color of the animation, used in battle spell effects, e.g. Cloned objects
-	void shiftColor(const ColorShifter * shifter);
+	void shiftColor(const ColorFilter & shifter);
 
 	//add custom surface to the selected position.
 	void setCustom(std::string filename, size_t frame, size_t group=0);

+ 2 - 2
client/gui/CIntObject.cpp

@@ -267,7 +267,7 @@ void CIntObject::addChild(CIntObject * child, bool adjustPosition)
 	children.push_back(child);
 	child->parent_m = this;
 	if(adjustPosition)
-		child->pos += pos;
+		child->pos += pos.topLeft();
 
 	if (!active && child->active)
 		child->deactivate();
@@ -289,7 +289,7 @@ void CIntObject::removeChild(CIntObject * child, bool adjustPosition)
 	children -= child;
 	child->parent_m = nullptr;
 	if(adjustPosition)
-		child->pos -= pos;
+		child->pos -= pos.topLeft();
 }
 
 void CIntObject::redraw()

+ 115 - 0
client/gui/ColorFilter.cpp

@@ -0,0 +1,115 @@
+/*
+ * Canvas.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 "ColorFilter.h"
+
+#include <SDL2/SDL_pixels.h>
+
+SDL_Color ColorFilter::shiftColor(const SDL_Color & in) const
+{
+	SDL_Color out;
+	out.r = in.r * r.r + in.g * r.g + in.b * r.b + 255 * r.a;
+	out.g = in.r * g.r + in.g * g.g + in.b * g.b + 255 * g.a;
+	out.b = in.r * b.r + in.g * b.g + in.b * b.b + 255 * b.a;
+	out.a = in.a * a;
+	return out;
+}
+
+bool ColorFilter::operator == (const ColorFilter & other) const
+{
+	return
+		r.r == other.r.r && r.g && other.r.g && r.b == other.r.b && r.a == other.r.a &&
+		g.r == other.g.r && g.g && other.g.g && g.b == other.g.b && g.a == other.g.a &&
+		b.r == other.b.r && b.g && other.b.g && b.b == other.b.b && b.a == other.b.a &&
+		a == other.a;
+}
+
+ColorFilter ColorFilter::genEmptyShifter( )
+{
+	return genAlphaShifter( 1.f);
+}
+
+ColorFilter ColorFilter::genAlphaShifter( float alpha )
+{
+	return genMuxerShifter(
+				{ 1.f, 0.f, 0.f, 0.f },
+				{ 0.f, 1.f, 0.f, 0.f },
+				{ 0.f, 0.f, 1.f, 0.f },
+				alpha);
+}
+
+ColorFilter ColorFilter::genGrayscaleShifter( )
+{
+	ChannelMuxer gray({0.299f, 0.587f, 0.114f, 0.f});
+
+	return genMuxerShifter(gray, gray, gray, 1.f);
+}
+
+ColorFilter ColorFilter::genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB )
+{
+	return genMuxerShifter(
+				{ maxR - minR, 0.f, 0.f, minR },
+				{ 0.f, maxG - minG, 0.f, minG },
+				{ 0.f, 0.f, maxB - minB, minB },
+				  1.f);
+}
+
+ColorFilter ColorFilter::genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a )
+{
+	return ColorFilter(r, g, b, a);
+}
+
+ColorFilter ColorFilter::genInterpolated(const ColorFilter & left, const ColorFilter & right, float power)
+{
+	auto lerpMuxer = [=]( const ChannelMuxer & left, const ChannelMuxer & right ) -> ChannelMuxer
+	{
+		return {
+			vstd::lerp(left.r, right.r, power),
+			vstd::lerp(left.g, right.g, power),
+			vstd::lerp(left.b, right.b, power),
+			vstd::lerp(left.a, right.a, power)
+		};
+	};
+
+	return genMuxerShifter(
+		lerpMuxer(left.r, right.r),
+		lerpMuxer(left.g, right.g),
+		lerpMuxer(left.b, right.b),
+		vstd::lerp(left.a, right.a, power)
+	);
+}
+
+ColorFilter ColorFilter::genCombined(const ColorFilter & left, const ColorFilter & right)
+{
+	// matrix multiplication
+	ChannelMuxer r{
+		left.r.r * right.r.r + left.g.r * right.r.g + left.b.r * right.r.b,
+		left.r.g * right.r.r + left.g.g * right.r.g + left.b.g * right.r.b,
+		left.r.b * right.r.r + left.g.b * right.r.g + left.b.b * right.r.b,
+		left.r.a * right.r.r + left.g.a * right.r.g + left.b.a * right.r.b + 1.f * right.r.a,
+	};
+
+	ChannelMuxer g{
+		left.r.r * right.g.r + left.g.r * right.g.g + left.b.r * right.g.b,
+		left.r.g * right.g.r + left.g.g * right.g.g + left.b.g * right.g.b,
+		left.r.b * right.g.r + left.g.b * right.g.g + left.b.b * right.g.b,
+		left.r.a * right.g.r + left.g.a * right.g.g + left.b.a * right.g.b + 1.f * right.g.a,
+	};
+
+	ChannelMuxer b{
+		left.r.r * right.b.r + left.g.r * right.b.g + left.b.r * right.b.b,
+		left.r.g * right.b.r + left.g.g * right.b.g + left.b.g * right.b.b,
+		left.r.b * right.b.r + left.g.b * right.b.g + left.b.b * right.b.b,
+		left.r.a * right.b.r + left.g.a * right.b.g + left.b.a * right.b.b + 1.f * right.b.a,
+	};
+
+	float a = left.a * right.a;
+	return genMuxerShifter(r,g,b,a);
+}

+ 55 - 0
client/gui/ColorFilter.h

@@ -0,0 +1,55 @@
+/*
+ * ColorFilter.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
+ *
+ */
+
+struct SDL_Color;
+
+#pragma once
+
+/// Base class for applying palette transformation on images
+class ColorFilter
+{
+	struct ChannelMuxer {
+		float r, g, b, a;
+	};
+
+	ChannelMuxer r;
+	ChannelMuxer g;
+	ChannelMuxer b;
+	float a;
+
+	ColorFilter(ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a):
+		r(r), g(g), b(b), a(a)
+	{}
+public:
+	SDL_Color shiftColor(const SDL_Color & in) const;
+
+	bool operator == (const ColorFilter & other) const;
+
+	/// Generates empty object that has no effect on image
+	static ColorFilter genEmptyShifter();
+
+	/// Generates object that changes alpha (transparency) of the image
+	static ColorFilter genAlphaShifter( float alpha );
+
+	/// Generates object that applies grayscale effect to image
+	static ColorFilter genGrayscaleShifter( );
+
+	/// Generates object that transforms each channel independently
+	static ColorFilter genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB );
+
+	/// Generates object that performs arbitrary mixing between any channels
+	static ColorFilter genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a );
+
+	/// Combines 2 mixers into a single object
+	static ColorFilter genCombined(const ColorFilter & left, const ColorFilter & right);
+
+	/// Scales down strength of a shifter to a specified factor
+	static ColorFilter genInterpolated(const ColorFilter & left, const ColorFilter & right, float power);
+};

+ 7 - 1
client/gui/Geometries.cpp

@@ -11,6 +11,11 @@
 #include "Geometries.h"
 #include "../CMT.h"
 #include <SDL_events.h>
+#include "../../lib/int3.h"
+
+Point::Point(const int3 &a)
+	:x(a.x),y(a.y)
+{}
 
 Point::Point(const SDL_MouseMotionEvent &a)
 	:x(a.x),y(a.y)
@@ -21,7 +26,7 @@ Rect Rect::createCentered( int w, int h )
 	return Rect(screen->w/2 - w/2, screen->h/2 - h/2, w, h);
 }
 
-Rect Rect::around(const Rect &r, int width) /*creates rect around another */
+Rect Rect::around(const Rect &r, int width)
 {
 	return Rect(r.x - width, r.y - width, r.w + width * 2, r.h + width * 2);
 }
@@ -30,3 +35,4 @@ Rect Rect::centerIn(const Rect &r)
 {
 	return Rect(r.x + (r.w - w) / 2, r.y + (r.h - h) / 2, w, h);
 }
+

+ 33 - 54
client/gui/Geometries.h

@@ -9,13 +9,16 @@
  */
 #pragma once
 
-#include <SDL_video.h>
-#include "../../lib/int3.h"
+#include <SDL2/SDL_rect.h>
 
 enum class ETextAlignment {TOPLEFT, CENTER, BOTTOMRIGHT};
 
 struct SDL_MouseMotionEvent;
 
+VCMI_LIB_NAMESPACE_BEGIN
+class int3;
+VCMI_LIB_NAMESPACE_END
+
 // A point with x/y coordinate, used mostly for graphic rendering
 struct Point
 {
@@ -26,13 +29,14 @@ struct Point
 	{
 		x = y = 0;
 	};
+
 	Point(int X, int Y)
 		:x(X),y(Y)
 	{};
-	Point(const int3 &a)
-		:x(a.x),y(a.y)
-	{}
-	Point(const SDL_MouseMotionEvent &a);
+
+	Point(const int3 &a);
+
+	explicit Point(const SDL_MouseMotionEvent &a);
 
 	template<typename T>
 	Point operator+(const T &b) const
@@ -73,10 +77,7 @@ struct Point
 		y -= b.y;
 		return *this;
 	}
-	bool operator<(const Point &b) const //product order
-	{
-		return x < b.x   &&   y < b.y;
-	}
+
 	template<typename T> Point& operator=(const T &t)
 	{
 		x = t.x;
@@ -96,7 +97,7 @@ struct Point
 /// Rectangle class, which have a position and a size
 struct Rect : public SDL_Rect
 {
-	Rect()//default c-tor
+	Rect()
 	{
 		x = y = w = h = -1;
 	}
@@ -121,42 +122,35 @@ struct Rect : public SDL_Rect
 		w = r.w;
 		h = r.h;
 	}
-	Rect(const Rect& r) : Rect(static_cast<const SDL_Rect&>(r))
-	{}
-	explicit Rect(const SDL_Surface * const &surf)
-	{
-		x = y = 0;
-		w = surf->w;
-		h = surf->h;
-	}
+	Rect(const Rect& r) = default;
 
 	Rect centerIn(const Rect &r);
 	static Rect createCentered(int w, int h);
-	static Rect around(const Rect &r, int width = 1); //creates rect around another
+	static Rect around(const Rect &r, int width = 1);
 
-	bool isIn(int qx, int qy) const //determines if given point lies inside rect
+	bool isIn(int qx, int qy) const
 	{
 		if (qx > x   &&   qx<x+w   &&   qy>y   &&   qy<y+h)
 			return true;
 		return false;
 	}
-	bool isIn(const Point & q) const //determines if given point lies inside rect
+	bool isIn(const Point & q) const
 	{
 		return isIn(q.x,q.y);
 	}
-	Point topLeft() const //top left corner of this rect
+	Point topLeft() const
 	{
 		return Point(x,y);
 	}
-	Point topRight() const //top right corner of this rect
+	Point topRight() const
 	{
 		return Point(x+w,y);
 	}
-	Point bottomLeft() const //bottom left corner of this rect
+	Point bottomLeft() const
 	{
 		return Point(x,y+h);
 	}
-	Point bottomRight() const //bottom right corner of this rect
+	Point bottomRight() const
 	{
 		return Point(x+w,y+h);
 	}
@@ -168,21 +162,19 @@ struct Rect : public SDL_Rect
 	{
 		return Point(w,h);
 	}
-	Rect operator+(const Rect &p) const //moves this rect by p's rect position
+
+	void moveTo(const Point & dest)
 	{
-		return Rect(x+p.x,y+p.y,w,h);
+		x = dest.x;
+		y = dest.y;
 	}
-	Rect operator+(const Point &p) const //moves this rect by p's point position
+
+	Rect operator+(const Point &p) const
 	{
 		return Rect(x+p.x,y+p.y,w,h);
 	}
-	Rect& operator=(const Point &p) //assignment operator
-	{
-		x = p.x;
-		y = p.y;
-		return *this;
-	}
-	Rect& operator=(const Rect &p) //assignment operator
+
+	Rect& operator=(const Rect &p)
 	{
 		x = p.x;
 		y = p.y;
@@ -190,34 +182,21 @@ struct Rect : public SDL_Rect
 		h = p.h;
 		return *this;
 	}
-	Rect& operator+=(const Rect &p) //works as operator+
-	{
-		x += p.x;
-		y += p.y;
-		return *this;
-	}
-	Rect& operator+=(const Point &p) //works as operator+
+
+	Rect& operator+=(const Point &p)
 	{
 		x += p.x;
 		y += p.y;
 		return *this;
 	}
-	Rect& operator-=(const Rect &p) //works as operator+
-	{
-		x -= p.x;
-		y -= p.y;
-		return *this;
-	}
-	Rect& operator-=(const Point &p) //works as operator+
+
+	Rect& operator-=(const Point &p)
 	{
 		x -= p.x;
 		y -= p.y;
 		return *this;
 	}
-	template<typename T> Rect operator-(const T &t)
-	{
-		return Rect(x - t.x, y - t.y, w, h);
-	}
+
 	Rect operator&(const Rect &p) const //rect intersection
 	{
 		bool intersect = true;

+ 10 - 11
client/gui/SDL_Extensions.cpp

@@ -349,12 +349,12 @@ static void drawLineX(SDL_Surface * sur, int x1, int y1, int x2, int y2, const S
 	for(int x = x1; x <= x2; x++)
 	{
 		float f = float(x - x1) / float(x2 - x1);
-		int y = CSDL_Ext::lerp(y1, y2, f);
+		int y = vstd::lerp(y1, y2, f);
 
-		uint8_t r = CSDL_Ext::lerp(color1.r, color2.r, f);
-		uint8_t g = CSDL_Ext::lerp(color1.g, color2.g, f);
-		uint8_t b = CSDL_Ext::lerp(color1.b, color2.b, f);
-		uint8_t a = CSDL_Ext::lerp(color1.a, color2.a, f);
+		uint8_t r = vstd::lerp(color1.r, color2.r, f);
+		uint8_t g = vstd::lerp(color1.g, color2.g, f);
+		uint8_t b = vstd::lerp(color1.b, color2.b, f);
+		uint8_t a = vstd::lerp(color1.a, color2.a, f);
 
 		Uint8 *p = CSDL_Ext::getPxPtr(sur, x, y);
 		ColorPutter<4, 0>::PutColor(p, r,g,b,a);
@@ -366,12 +366,12 @@ static void drawLineY(SDL_Surface * sur, int x1, int y1, int x2, int y2, const S
 	for(int y = y1; y <= y2; y++)
 	{
 		float f = float(y - y1) / float(y2 - y1);
-		int x = CSDL_Ext::lerp(x1, x2, f);
+		int x = vstd::lerp(x1, x2, f);
 
-		uint8_t r = CSDL_Ext::lerp(color1.r, color2.r, f);
-		uint8_t g = CSDL_Ext::lerp(color1.g, color2.g, f);
-		uint8_t b = CSDL_Ext::lerp(color1.b, color2.b, f);
-		uint8_t a = CSDL_Ext::lerp(color1.a, color2.a, f);
+		uint8_t r = vstd::lerp(color1.r, color2.r, f);
+		uint8_t g = vstd::lerp(color1.g, color2.g, f);
+		uint8_t b = vstd::lerp(color1.b, color2.b, f);
+		uint8_t a = vstd::lerp(color1.a, color2.a, f);
 
 		Uint8 *p = CSDL_Ext::getPxPtr(sur, x, y);
 		ColorPutter<4, 0>::PutColor(p, r,g,b,a);
@@ -922,7 +922,6 @@ void CSDL_Ext::setDefaultColorKeyPresize(SDL_Surface * surface)
 }
 
 
-
 template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<2>(int, int);
 template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<3>(int, int);
 template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<4>(int, int);

+ 0 - 82
client/gui/SDL_Extensions.h

@@ -49,12 +49,6 @@ inline bool isShiftKeyDown()
 }
 namespace CSDL_Ext
 {
-	template<typename Int>
-	Int lerp(Int a, Int b, float f)
-	{
-		return a + std::round((b - a) * f);
-	}
-
 	//todo: should this better be assignment operator?
 	STRONG_INLINE void colorAssign(SDL_Color & dest, const SDL_Color & source)
 	{
@@ -158,82 +152,6 @@ struct ColorPutter
 
 typedef void (*BlitterWithRotationVal)(SDL_Surface *src,SDL_Rect srcRect, SDL_Surface * dst, SDL_Rect dstRect, ui8 rotation);
 
-/// Base class for applying palette transformation on images
-class ColorShifter
-{
-public:
-	~ColorShifter() = default;
-	virtual SDL_Color shiftColor(SDL_Color input) const = 0;
-};
-
-/// Generic class for palette transformation
-/// Applies linear transformation to move all colors into range (min, max)
-class ColorShifterRange : public ColorShifter
-{
-	SDL_Color base;
-	SDL_Color factor;
-
-public:
-	ColorShifterRange(SDL_Color min, SDL_Color max) :
-		base(min)
-	{
-		assert(max.r >= min.r);
-		assert(max.g >= min.g);
-		assert(max.b >= min.b);
-		assert(max.a >= min.a);
-		factor.r = max.r - min.r;
-		factor.g = max.g - min.g;
-		factor.b = max.b - min.b;
-		factor.a = max.a - min.a;
-	}
-
-	SDL_Color shiftColor(SDL_Color input) const override
-	{
-		return {
-			uint8_t(base.r + input.r * factor.r / 255),
-			uint8_t(base.g + input.g * factor.g / 255),
-			uint8_t(base.b + input.b * factor.b / 255),
-			uint8_t(base.a + input.a * factor.a / 255),
-		};
-	}
-};
-
-/// Color shifter that allows to specify color to be excempt from changes
-class ColorShifterRangeExcept : public ColorShifterRange
-{
-	SDL_Color ignored;
-public:
-	ColorShifterRangeExcept(SDL_Color min, SDL_Color max, SDL_Color ignored) :
-		ColorShifterRange(min, max),
-		ignored(ignored)
-	{}
-
-	SDL_Color shiftColor(SDL_Color input) const override
-	{
-		if ( input.r == ignored.r && input.g == ignored.g && input.b == ignored.b && input.a == ignored.a)
-			return input;
-		return ColorShifterRange::shiftColor(input);
-	}
-};
-
-class ColorShifterGrayscale : public ColorShifter
-{
-public:
-	SDL_Color shiftColor(SDL_Color input) const override
-	{
-		// Apply grayscale conversion according to human eye perception values
-		uint32_t gray = static_cast<uint32_t>(0.299 * input.r + 0.587 * input.g + 0.114 * input.b);
-		assert(gray < 256);
-
-		return {
-			uint8_t(gray),
-			uint8_t(gray),
-			uint8_t(gray),
-			input.a
-		};
-	}
-};
-
 namespace CSDL_Ext
 {
 	/// helper that will safely set and un-set ClipRect for SDL_Surface

+ 2 - 2
client/lobby/CSelectionBase.cpp

@@ -307,7 +307,7 @@ CChatBox::CChatBox(const Rect & rect)
 	: CIntObject(KEYBOARD | TEXTINPUT)
 {
 	OBJ_CONSTRUCTION;
-	pos += rect;
+	pos += rect.topLeft();
 	captureAllKeys = true;
 	type |= REDRAW_PARENT;
 
@@ -341,7 +341,7 @@ void CChatBox::addNewMessage(const std::string & text)
 CFlagBox::CFlagBox(const Rect & rect)
 	: CIntObject(RCLICK)
 {
-	pos += rect;
+	pos += rect.topLeft();
 	pos.w = rect.w;
 	pos.h = rect.h;
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;

+ 2 - 2
client/widgets/AdventureMapClasses.cpp

@@ -236,7 +236,7 @@ void CHeroList::CHeroItem::open()
 
 void CHeroList::CHeroItem::showTooltip()
 {
-	CRClickPopup::createAndPush(hero, GH.current->motion);
+	CRClickPopup::createAndPush(hero, Point(GH.current->motion));
 }
 
 std::string CHeroList::CHeroItem::getHoverText()
@@ -328,7 +328,7 @@ void CTownList::CTownItem::open()
 
 void CTownList::CTownItem::showTooltip()
 {
-	CRClickPopup::createAndPush(town, GH.current->motion);
+	CRClickPopup::createAndPush(town, Point(GH.current->motion));
 }
 
 std::string CTownList::CTownItem::getHoverText()

+ 1 - 0
client/widgets/AdventureMapClasses.h

@@ -11,6 +11,7 @@
 
 #include "ObjectLists.h"
 #include "../../lib/FunctionList.h"
+#include "../../lib/int3.h"
 #include "Terrain.h"
 
 VCMI_LIB_NAMESPACE_BEGIN

+ 2 - 2
client/widgets/CComponent.cpp

@@ -443,7 +443,7 @@ CComponentBox::CComponentBox(std::vector<std::shared_ptr<CComponent>> _component
 	components(_components)
 {
 	type |= REDRAW_PARENT;
-	pos = position + pos;
+	pos = position + pos.topLeft();
 	placeComponents(false);
 }
 
@@ -452,7 +452,7 @@ CComponentBox::CComponentBox(std::vector<std::shared_ptr<CSelectableComponent>>
 	onSelect(_onSelect)
 {
 	type |= REDRAW_PARENT;
-	pos = position + pos;
+	pos = position + pos.topLeft();
 	placeComponents(true);
 
 	assert(!components.empty());

+ 1 - 1
client/widgets/Images.cpp

@@ -167,7 +167,7 @@ void CPicture::scaleTo(Point size)
 
 void CPicture::createSimpleRect(const Rect &r, bool screenFormat, ui32 color)
 {
-	pos += r;
+	pos += r.topLeft();
 	pos.w = r.w;
 	pos.h = r.h;
 	if(screenFormat)

+ 2 - 2
client/widgets/MiscWidgets.cpp

@@ -68,7 +68,7 @@ LRClickableAreaWText::LRClickableAreaWText()
 LRClickableAreaWText::LRClickableAreaWText(const Rect &Pos, const std::string &HoverText, const std::string &ClickText)
 {
 	init();
-	pos = Pos + pos;
+	pos = Pos + pos.topLeft();
 	hoverText = HoverText;
 	text = ClickText;
 }
@@ -430,7 +430,7 @@ MoraleLuckBox::MoraleLuckBox(bool Morale, const Rect &r, bool Small)
 	small(Small)
 {
 	bonusValue = 0;
-	pos = r + pos;
+	pos = r + pos.topLeft();
 	defActions = 255-DISPOSE;
 }
 

+ 2 - 2
client/widgets/TextControls.cpp

@@ -473,7 +473,7 @@ CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList<void(c
 CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const std::string & bgName, const CFunctionList<void(const std::string &)> & CB)
 	:cb(CB), 	CFocusable(std::make_shared<CKeyboardFocusListener>(this))
 {
-	pos += Pos;
+	pos += Pos.topLeft();
 	pos.h = Pos.h;
 	pos.w = Pos.w;
 
@@ -490,7 +490,7 @@ CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const std::stri
 CTextInput::CTextInput(const Rect & Pos, SDL_Surface * srf)
 	:CFocusable(std::make_shared<CKeyboardFocusListener>(this))
 {
-	pos += Pos;
+	pos += Pos.topLeft();
 	captureAllKeys = true;
 	OBJ_CONSTRUCTION;
 	background = std::make_shared<CPicture>(Pos, 0, true);

+ 1 - 1
client/windows/CAdvmapInterface.cpp

@@ -1810,7 +1810,7 @@ void CAdvMapInt::tileRClicked(const int3 &mapPos)
 		return;
 	}
 
-	CRClickPopup::createAndPush(obj, GH.current->motion, ETextAlignment::CENTER);
+	CRClickPopup::createAndPush(obj, Point(GH.current->motion), ETextAlignment::CENTER);
 }
 
 void CAdvMapInt::enterCastingMode(const CSpell * sp)