浏览代码

Merge remote-tracking branch 'upstream/develop' into develop

Xilmi 1 年之前
父节点
当前提交
b83a214763

+ 6 - 0
AI/Nullkiller/AIGateway.cpp

@@ -1311,6 +1311,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;
@@ -1332,6 +1337,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 				status.setChannelProbing(true);
 				for(auto exit : teleportChannelProbingList)
 					doTeleportMovement(exit, int3(-1));
+
 				teleportChannelProbingList.clear();
 				status.setChannelProbing(false);
 

+ 6 - 3
AI/Nullkiller/Behaviors/ExplorationBehavior.cpp

@@ -47,10 +47,13 @@ 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::SUBTERRANEAN_GATE:
+		case Obj::WHIRLPOOL:
 			{
-				auto tObj = dynamic_cast<const CGTeleport*>(obj);
-				for (auto exit : cb->getTeleportChannelExits(tObj->channel))
+				auto tObj = dynamic_cast<const CGTeleport *>(obj);
+				if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability)
+					break;
+				for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits)
 				{
 					if (exit != tObj->id)
 					{

+ 2 - 0
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

+ 20 - 0
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

+ 15 - 5
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)
+{
+	addSingleCreatureStacks(hero);
+}
+
+void ArmyFormation::addSingleCreatureStacks(const CGHeroInstance * hero)
 {
-	auto freeSlots = attacker->getFreeSlotsQueue();
+	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)
 	{

+ 4 - 0
AI/Nullkiller/Helpers/ArmyFormation.h

@@ -31,6 +31,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);
 };
 
 }

+ 40 - 4
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;
 

+ 2 - 0
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;

+ 55 - 0
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());
+}*/
+
+}

+ 35 - 0
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;
+	};
+}
+}

+ 2 - 0
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
 {

+ 3 - 1
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());
 

+ 1 - 0
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();

+ 1 - 0
lib/pathfinder/PathfinderOptions.cpp

@@ -34,6 +34,7 @@ PathfinderOptions::PathfinderOptions()
 	, turnLimit(std::numeric_limits<uint8_t>::max())
 	, canUseCast(false)
 	, allowLayerTransitioningAfterBattle(false)
+	, forceUseTeleportWhirlpool(false)
 {
 }
 

+ 1 - 0
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.