diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp
index ec1135ee0..c6ac28eaa 100644
--- a/AI/Nullkiller/AIGateway.cpp
+++ b/AI/Nullkiller/AIGateway.cpp
@@ -1310,6 +1310,11 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 
 		auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos)
 		{
+			if(cb->getObj(exitId) && cb->getObj(exitId)->ID == Obj::WHIRLPOOL)
+			{
+				nullkiller->armyFormation->rearrangeArmyForWhirlpool(*h);
+			}
+
 			destinationTeleport = exitId;
 			if(exitPos.valid())
 				destinationTeleportPos = exitPos;
@@ -1331,6 +1336,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 				status.setChannelProbing(true);
 				for(auto exit : teleportChannelProbingList)
 					doTeleportMovement(exit, int3(-1));
+
 				teleportChannelProbingList.clear();
 				status.setChannelProbing(false);
 
diff --git a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp
index cab2707f3..4a210d2c0 100644
--- a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp
+++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp
@@ -46,6 +46,7 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
 			case Obj::MONOLITH_ONE_WAY_ENTRANCE:
 			case Obj::MONOLITH_TWO_WAY:
 			case Obj::SUBTERRANEAN_GATE:
+			case Obj::WHIRLPOOL:
 				auto tObj = dynamic_cast<const CGTeleport *>(obj);
 				if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability)
 				{
@@ -60,6 +61,7 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
 			{
 			case Obj::MONOLITH_TWO_WAY:
 			case Obj::SUBTERRANEAN_GATE:
+			case Obj::WHIRLPOOL:
 				auto tObj = dynamic_cast<const CGTeleport *>(obj);
 				if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability)
 					break;
diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt
index 8ea2634b4..7d9f272a1 100644
--- a/AI/Nullkiller/CMakeLists.txt
+++ b/AI/Nullkiller/CMakeLists.txt
@@ -8,6 +8,7 @@ set(Nullkiller_SRCS
 		Pathfinding/Actions/QuestAction.cpp
 		Pathfinding/Actions/BuyArmyAction.cpp
 		Pathfinding/Actions/BoatActions.cpp
+		Pathfinding/Actions/WhirlpoolAction.cpp
 		Pathfinding/Actions/TownPortalAction.cpp
 		Pathfinding/Actions/AdventureSpellCastMovementActions.cpp
 		Pathfinding/Rules/AILayerTransitionRule.cpp
@@ -79,6 +80,7 @@ set(Nullkiller_HEADERS
 		Pathfinding/Actions/QuestAction.h
 		Pathfinding/Actions/BuyArmyAction.h
 		Pathfinding/Actions/BoatActions.h
+		Pathfinding/Actions/WhirlpoolAction.h
 		Pathfinding/Actions/TownPortalAction.h
 		Pathfinding/Actions/AdventureSpellCastMovementActions.h
 		Pathfinding/Rules/AILayerTransitionRule.h
diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp
index 95b5ca725..8fe4851b2 100644
--- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp
+++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp
@@ -196,6 +196,26 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 					}
 				}
 
+				auto findWhirlpool = [&ai](const int3 & pos) -> ObjectInstanceID
+				{
+					auto objs = ai->myCb->getVisitableObjs(pos);
+					auto whirlpool = std::find_if(objs.begin(), objs.end(), [](const CGObjectInstance * o)->bool
+						{
+							return o->ID == Obj::WHIRLPOOL;
+						});
+
+					return whirlpool != objs.end() ? dynamic_cast<const CGWhirlpool *>(*whirlpool)->id : ObjectInstanceID(-1);
+				};
+
+				auto sourceWhirlpool = findWhirlpool(hero->visitablePos());
+				auto targetWhirlpool = findWhirlpool(node->coord);
+				
+				if(i != chainPath.nodes.size() - 1 && sourceWhirlpool.hasValue() && sourceWhirlpool == targetWhirlpool)
+				{
+					logAi->trace("AI exited whirlpool at %s but expected at %s", hero->visitablePos().toString(), node->coord.toString());
+					continue;
+				}
+
 				if(hero->movementPointsRemaining())
 				{
 					try
diff --git a/AI/Nullkiller/Helpers/ArmyFormation.cpp b/AI/Nullkiller/Helpers/ArmyFormation.cpp
index 464cb10d0..26d8c7d40 100644
--- a/AI/Nullkiller/Helpers/ArmyFormation.cpp
+++ b/AI/Nullkiller/Helpers/ArmyFormation.cpp
@@ -14,27 +14,37 @@
 namespace NKAI
 {
 
-void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker)
+void ArmyFormation::rearrangeArmyForWhirlpool(const CGHeroInstance * hero)
 {
-	auto freeSlots = attacker->getFreeSlotsQueue();
+	addSingleCreatureStacks(hero);
+}
+
+void ArmyFormation::addSingleCreatureStacks(const CGHeroInstance * hero)
+{
+	auto freeSlots = hero->getFreeSlotsQueue();
 
 	while(!freeSlots.empty())
 	{
-		auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair<SlotID, CStackInstance *> & slot) -> int
+		auto weakestCreature = vstd::minElementByFun(hero->Slots(), [](const std::pair<SlotID, CStackInstance *> & slot) -> int
 			{
 				return slot.second->getCount() == 1
 					? std::numeric_limits<int>::max()
 					: slot.second->getCreatureID().toCreature()->getAIValue();
 			});
 
-		if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1)
+		if(weakestCreature == hero->Slots().end() || weakestCreature->second->getCount() == 1)
 		{
 			break;
 		}
 
-		cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1);
+		cb->splitStack(hero, hero, weakestCreature->first, freeSlots.front(), 1);
 		freeSlots.pop();
 	}
+}
+
+void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker)
+{
+	addSingleCreatureStacks(attacker);
 
 	if(town->fortLevel() > CGTownInstance::FORT)
 	{
diff --git a/AI/Nullkiller/Helpers/ArmyFormation.h b/AI/Nullkiller/Helpers/ArmyFormation.h
index 817c6158d..79fec0f2a 100644
--- a/AI/Nullkiller/Helpers/ArmyFormation.h
+++ b/AI/Nullkiller/Helpers/ArmyFormation.h
@@ -33,6 +33,10 @@ public:
 	ArmyFormation(std::shared_ptr<CCallback> CB, const Nullkiller * ai): cb(CB) {}
 
 	void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker);
+
+	void rearrangeArmyForWhirlpool(const CGHeroInstance * hero);
+
+	void addSingleCreatureStacks(const CGHeroInstance * hero);
 };
 
 }
diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp
index b4fb42cf2..326eda3d1 100644
--- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp
+++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp
@@ -10,6 +10,7 @@
 #include "StdInc.h"
 #include "AINodeStorage.h"
 #include "Actions/TownPortalAction.h"
+#include "Actions/WhirlpoolAction.h"
 #include "../Goals/Goals.h"
 #include "../AIGateway.h"
 #include "../Engine/Nullkiller.h"
@@ -255,10 +256,45 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf
 	{
 		commit(dstNode, srcNode, destination.action, destination.turn, destination.movementLeft, destination.cost);
 
-		if(srcNode->specialAction || srcNode->chainOther)
+		// regular pathfinder can not go directly through whirlpool
+		bool isWhirlpoolTeleport = destination.nodeObject
+			&& destination.nodeObject->ID == Obj::WHIRLPOOL;
+
+		if(srcNode->specialAction
+			|| srcNode->chainOther
+			|| isWhirlpoolTeleport)
 		{
 			// there is some action on source tile which should be performed before we can bypass it
-			destination.node->theNodeBefore = source.node;
+			dstNode->theNodeBefore = source.node;
+
+			if(isWhirlpoolTeleport)
+			{
+				if(dstNode->actor->creatureSet->Slots().size() == 1
+					&& dstNode->actor->creatureSet->Slots().begin()->second->getCount() == 1)
+				{
+					return;
+				}
+
+				auto weakest = vstd::minElementByFun(dstNode->actor->creatureSet->Slots(), [](std::pair<SlotID, const CStackInstance *> pair) -> int
+					{
+						return pair.second->getCount() * pair.second->getCreatureID().toCreature()->getAIValue();
+					});
+
+				if(weakest == dstNode->actor->creatureSet->Slots().end())
+				{
+					logAi->debug("Empty army entering whirlpool detected at tile %s", dstNode->coord.toString());
+					destination.blocked = true;
+
+					return;
+				}
+
+				if(dstNode->actor->creatureSet->getFreeSlots().size())
+					dstNode->armyLoss += weakest->second->getCreatureID().toCreature()->getAIValue();
+				else
+					dstNode->armyLoss += (weakest->second->getCount() + 1) / 2 * weakest->second->getCreatureID().toCreature()->getAIValue();
+
+				dstNode->specialAction = AIPathfinding::WhirlpoolAction::instance;
+			}
 		}
 
 		if(dstNode->specialAction && dstNode->actor)
@@ -1014,8 +1050,8 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
 
 		for(auto & neighbour : accessibleExits)
 		{
-			auto node = getOrCreateNode(neighbour, source.node->layer, srcNode->actor);
-
+			std::optional<AIPathNode *> node = getOrCreateNode(neighbour, source.node->layer, srcNode->actor);
+			
 			if(!node)
 				continue;
 
diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
index d4a3a651a..863d8975a 100644
--- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
+++ b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
@@ -48,6 +48,8 @@ namespace AIPathfinding
 	{
 		options.canUseCast = true;
 		options.allowLayerTransitioningAfterBattle = true;
+		options.useTeleportWhirlpool = true;
+		options.forceUseTeleportWhirlpool = true;
 	}
 
 	AIPathfinderConfig::~AIPathfinderConfig() = default;
diff --git a/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp
new file mode 100644
index 000000000..56625a1b3
--- /dev/null
+++ b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp
@@ -0,0 +1,55 @@
+/*
+* WhirlpoolAction.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 "../../Goals/AdventureSpellCast.h"
+#include "../../../../lib/mapObjects/MapObjects.h"
+#include "WhirlpoolAction.h"
+#include "../../AIGateway.h"
+
+namespace NKAI
+{
+
+using namespace AIPathfinding;
+
+std::shared_ptr<WhirlpoolAction> WhirlpoolAction::instance = std::make_shared<WhirlpoolAction>();
+
+void WhirlpoolAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
+{
+	ai->nullkiller->armyFormation->rearrangeArmyForWhirlpool(hero);
+}
+
+std::string WhirlpoolAction::toString() const
+{
+	return "Prepare for whirlpool";
+}
+/*
+bool TownPortalAction::canAct(const CGHeroInstance * hero, const AIPathNode * source) const
+{
+#ifdef VCMI_TRACE_PATHFINDER
+	logAi->trace(
+		"Hero %s has %d mana and needed %d and already spent %d",
+		hero->name,
+		hero->mana,
+		getManaCost(hero),
+		source->manaCost);
+#endif
+
+	return hero->mana >= source->manaCost + getManaCost(hero);
+}
+
+uint32_t TownPortalAction::getManaCost(const CGHeroInstance * hero) const
+{
+	SpellID summonBoat = SpellID::TOWN_PORTAL;
+
+	return hero->getSpellCost(summonBoat.toSpell());
+}*/
+
+}
diff --git a/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h
new file mode 100644
index 000000000..704b94b0f
--- /dev/null
+++ b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h
@@ -0,0 +1,35 @@
+/*
+* WhirlpoolAction.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+
+#pragma once
+
+#include "SpecialAction.h"
+#include "../../../../lib/mapObjects/MapObjects.h"
+#include "../../Goals/AdventureSpellCast.h"
+
+namespace NKAI
+{
+namespace AIPathfinding
+{
+	class WhirlpoolAction : public SpecialAction
+	{
+	public:
+		WhirlpoolAction()
+		{
+		}
+
+		static std::shared_ptr<WhirlpoolAction> instance;
+
+		void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
+
+		std::string toString() const override;
+	};
+}
+}
diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
index 34f4f2766..88d61372c 100644
--- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
+++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
@@ -11,9 +11,11 @@
 #include "AIMovementAfterDestinationRule.h"
 #include "../Actions/BattleAction.h"
 #include "../Actions/QuestAction.h"
+#include "../Actions/WhirlpoolAction.h"
 #include "../../Goals/Invalid.h"
 #include "AIPreviousNodeRule.h"
 #include "../../../../lib/pathfinder/PathfinderOptions.h"
+#include "../../../../lib/pathfinder/CPathfinder.h"
 
 namespace NKAI
 {
diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp
index cefb55673..ef92af1df 100644
--- a/lib/pathfinder/CPathfinder.cpp
+++ b/lib/pathfinder/CPathfinder.cpp
@@ -466,7 +466,7 @@ bool CPathfinderHelper::addTeleportOneWayRandom(const CGTeleport * obj) const
 
 bool CPathfinderHelper::addTeleportWhirlpool(const CGWhirlpool * obj) const
 {
-	return options.useTeleportWhirlpool && hasBonusOfType(BonusType::WHIRLPOOL_PROTECTION) && obj;
+	return options.useTeleportWhirlpool && (whirlpoolProtection || options.forceUseTeleportWhirlpool) && obj;
 }
 
 int CPathfinderHelper::movementPointsAfterEmbark(int movement, int basicCost, bool disembark) const
@@ -506,6 +506,8 @@ CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Her
 	updateTurnInfo();
 	initializePatrol();
 
+	whirlpoolProtection = Hero->hasBonusOfType(BonusType::WHIRLPOOL_PROTECTION);
+
 	SpellID flySpell = SpellID::FLY;
 	canCastFly = Hero->canCastThisSpell(flySpell.toSpell());
 
diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h
index 518244b6d..4791a450f 100644
--- a/lib/pathfinder/CPathfinder.h
+++ b/lib/pathfinder/CPathfinder.h
@@ -85,6 +85,7 @@ public:
 	const PathfinderOptions & options;
 	bool canCastFly;
 	bool canCastWaterWalk;
+	bool whirlpoolProtection;
 
 	CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options);
 	virtual ~CPathfinderHelper();
diff --git a/lib/pathfinder/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp
index 675731c8c..bdc749c1a 100644
--- a/lib/pathfinder/PathfinderOptions.cpp
+++ b/lib/pathfinder/PathfinderOptions.cpp
@@ -34,6 +34,7 @@ PathfinderOptions::PathfinderOptions()
 	, turnLimit(std::numeric_limits<uint8_t>::max())
 	, canUseCast(false)
 	, allowLayerTransitioningAfterBattle(false)
+	, forceUseTeleportWhirlpool(false)
 {
 }
 
diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h
index 044511910..f2469c9d2 100644
--- a/lib/pathfinder/PathfinderOptions.h
+++ b/lib/pathfinder/PathfinderOptions.h
@@ -30,6 +30,7 @@ struct DLL_LINKAGE PathfinderOptions
 	bool useTeleportOneWay; // One-way monoliths with one known exit only
 	bool useTeleportOneWayRandom; // One-way monoliths with more than one known exit
 	bool useTeleportWhirlpool; // Force enabled if hero protected or unaffected (have one stack of one creature)
+	bool forceUseTeleportWhirlpool; // Force enabled if hero protected or unaffected (have one stack of one creature)
 
 							   /// TODO: Find out with client and server code, merge with normal teleporters.
 							   /// Likely proper implementation would require some refactoring of CGTeleport.