1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-22 22:13:35 +02:00

NKAI: whirlpool

This commit is contained in:
Andrii Danylchenko 2024-07-20 18:36:07 +03:00
parent 3c611ffa5b
commit 683c363946
15 changed files with 189 additions and 10 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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)
{

View File

@ -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);
};
}

View File

@ -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;

View File

@ -48,6 +48,8 @@ namespace AIPathfinding
{
options.canUseCast = true;
options.allowLayerTransitioningAfterBattle = true;
options.useTeleportWhirlpool = true;
options.forceUseTeleportWhirlpool = true;
}
AIPathfinderConfig::~AIPathfinderConfig() = default;

View File

@ -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());
}*/
}

View File

@ -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;
};
}
}

View File

@ -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
{

View File

@ -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());

View File

@ -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();

View File

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

View File

@ -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.