Преглед на файлове

Nullkiller: rework defence a bit

Andrii Danylchenko преди 4 години
родител
ревизия
84e5e6ac17

+ 1 - 1
AI/Nullkiller/AIUtility.h

@@ -31,7 +31,7 @@ const int ACTUAL_RESOURCE_COUNT = 7;
 const int ALLOWED_ROAMING_HEROES = 8;
 
 //implementation-dependent
-extern const double SAFE_ATTACK_CONSTANT;
+extern const float SAFE_ATTACK_CONSTANT;
 extern const int GOLD_RESERVE;
 
 //provisional class for AI to store a reference to an owned hero object

+ 75 - 38
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -39,11 +39,6 @@ Goals::TGoalVec DefenceBehavior::getTasks()
 
 	if(heroes.size())
 	{
-		auto mainArmy = *vstd::maxElementByFun(heroes, [](const CGHeroInstance * hero) -> uint64_t
-		{
-			return hero->getTotalStrength();
-		});
-
 		for(auto town : cb->getTownsInfo())
 		{
 			evaluateDefence(tasks, town);
@@ -53,13 +48,41 @@ Goals::TGoalVec DefenceBehavior::getTasks()
 	return tasks;
 }
 
+uint64_t townArmyIncome(const CGTownInstance * town)
+{
+	uint64_t result = 0;
+
+	for(auto creatureInfo : town->creatures)
+	{
+		if(creatureInfo.second.empty())
+			continue;
+
+		auto creature = creatureInfo.second.back().toCreature();
+		result += creature->AIValue * town->getGrowthInfo(creature->level).totalGrowth();
+	}
+
+	return result;
+}
+
 void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town)
 {
-	logAi->debug("Evaluating defence for %s", town->name);
+	auto basicPriority = 0.3f + std::sqrt(townArmyIncome(town) / 20000.0f)
+		+ town->dailyIncome()[Res::GOLD] / 10000.0f;
+
+	logAi->debug("Evaluating defence for %s, basic priority %f", town->name, basicPriority);
 
 	auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
 	auto treats = { treatNode.fastestDanger, treatNode.maximumDanger };
 
+	if(!treatNode.fastestDanger.hero)
+	{
+		logAi->debug("No treat found for town %s", town->name);
+
+		return;
+	}
+
+	int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
+
 	if(town->garrisonHero)
 	{
 		if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
@@ -75,17 +98,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 		return;
 	}
-
-	if(town->visitingHero && isSafeToVisit(town->visitingHero.get(), treatNode.maximumDanger.danger))
-	{
-		logAi->debug(
-			"Town %s has visiting hero %s who is strong enough to defend the town", 
-			town->name, 
-			town->visitingHero->name);
-
-		return;
-	}
-
+	
 	uint64_t reinforcement = ai->ah->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
 
 	if(reinforcement)
@@ -103,44 +116,68 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 		return;
 	}
 
-	for(AIPath & path : paths)
-	{
-		for(auto & treat : treats)
-		{
-			if(isSafeToVisit(path.targetHero, path.heroArmy, treat.danger))
-			{
-				logAi->debug(
-					"Hero %s can eliminate danger for town %s using path %s.", 
-					path.targetHero->name,
-					town->name,
-					path.toString());
-
-				return;
-			}
-		}
-	}
-
 	for(auto & treat : treats)
 	{
 		logAi->debug(
-			"Town %s has treat %lld in %s turns, hero: %s", 
+			"Town %s has treat %lld in %s turns, hero: %s",
 			town->name,
 			treat.danger,
 			std::to_string(treat.turn),
 			treat.hero->name);
 
+		bool treatIsUnderControl = false;
+
+		for(AIPath & path : paths)
+		{
+			if(path.getHeroStrength() > treat.danger)
+			{
+				if(dayOfWeek + treat.turn < 6 && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger)
+					|| path.exchangeCount == 1 && path.turn() < treat.turn
+					|| path.turn() < treat.turn - 1)
+				{
+					logAi->debug(
+						"Hero %s can eliminate danger for town %s using path %s.",
+						path.targetHero->name,
+						town->name,
+						path.toString());
+
+					treatIsUnderControl = true;
+					break;
+				}
+			}
+		}
+
+		if(treatIsUnderControl)
+			continue;
+
+		if(ai->canRecruitAnyHero(town))
+		{
+			auto heroesInTavern = cb->getAvailableHeroes(town);
+
+			for(auto hero : heroesInTavern)
+			{
+				if(hero->getTotalStrength() > treat.danger)
+				{
+					tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town).setobjid(hero->id.getNum()).setpriority(1)));
+					continue;
+				}
+			}
+		}
+
 		for(AIPath & path : paths)
 		{
 #if AI_TRACE_LEVEL >= 1
 			logAi->trace(
-				"Hero %s can defend town with force %lld in %s turns, path: %s",
+				"Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s",
 				path.targetHero->name,
 				path.getHeroStrength(),
 				std::to_string(path.turn()),
+				path.movementCost(),
 				path.toString());
 #endif
 
-			float priority = 0.6f + (float)path.getHeroStrength() / treat.danger / (treat.turn + 1);
+			float priority = basicPriority
+				+ std::min(SAFE_ATTACK_CONSTANT, (float)path.getHeroStrength() / treat.danger) / (treat.turn + 1);
 
 			if(path.targetHero == town->visitingHero && path.exchangeCount == 1)
 			{
@@ -156,7 +193,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				continue;
 			}
 				
-			if(path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger)
+			if(path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger)
 			{
 #if AI_TRACE_LEVEL >= 1
 				logAi->trace("Move %s to defend town %s with priority %f",

+ 8 - 4
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -31,13 +31,17 @@ std::string RecruitHeroBehavior::toString() const
 Goals::TGoalVec RecruitHeroBehavior::getTasks()
 {
 	Goals::TGoalVec tasks;
+	auto towns = cb->getTownsInfo();
 
-	if(ai->canRecruitAnyHero())
+	for(auto town : towns)
 	{
-		if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
-			|| cb->getResourceAmount(Res::GOLD) > 10000)
+		if(!town->garrisonHero && ai->canRecruitAnyHero(town))
 		{
-			tasks.push_back(Goals::sptr(Goals::RecruitHero()));
+			if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
+				|| cb->getResourceAmount(Res::GOLD) > 10000)
+			{
+				tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town)));
+			}
 		}
 	}
 

+ 25 - 4
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp

@@ -66,16 +66,37 @@ void ExchangeSwapTownHeroes::accept(VCAI * ai)
 	if(town->visitingHero && town->visitingHero.get() != garrisonHero)
 		cb->swapGarrisonHero(town);
 
+	makePossibleUpgrades(town);
 	ai->moveHeroToTile(town->visitablePos(), garrisonHero);
 
-	cb->swapGarrisonHero(town); // selected hero left in garrison with strongest army
-	ai->nullkiller->lockHero(town->garrisonHero.get());
+	auto upperArmy = town->getUpperArmy();
+	
+	if(!town->garrisonHero && upperArmy->stacksCount() != 0)
+	{
+		// dismiss creatures we are not able to pick to be able to hide in garrison
+		if(upperArmy->getArmyStrength() < 500 
+			&& town->fortLevel() >= CGTownInstance::CITADEL)
+		{
+			for(auto slot : upperArmy->Slots())
+			{
+				cb->dismissCreature(upperArmy, slot.first);
+			}
+
+			cb->swapGarrisonHero(town);
+		}
+	}
+	else
+	{
+		cb->swapGarrisonHero(town); // selected hero left in garrison with strongest army
+	}
+
+	ai->nullkiller->lockHero(garrisonHero);
 
-	if(town->visitingHero)
+	if(town->visitingHero && town->visitingHero != garrisonHero)
 	{
 		ai->nullkiller->unlockHero(town->visitingHero.get());
 		makePossibleUpgrades(town->visitingHero);
 	}
 
-	logAi->debug("Put hero %s to garrison of %s", town->garrisonHero->name, town->name);
+	logAi->debug("Put hero %s to garrison of %s", garrisonHero->name, town->name);
 }

+ 27 - 0
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -96,9 +96,36 @@ void ExecuteHeroChain::accept(VCAI * ai)
 					}
 				}
 
+				if(node.turns == 0)
+				{
+					auto targetNode = cb->getPathsInfo(hero.get())->getPathInfo(node.coord);
+
+					if(!targetNode->accessible || targetNode->turns != 0)
+					{
+						logAi->error(
+							"Enable to complete chain. Expected hero %s to arive to %s but he in 0 turns but he can not do this",
+							hero.name,
+							node.coord.toString(),
+							hero->visitablePos().toString());
+
+						return;
+					}
+				}
+
 				Goals::VisitTile(node.coord).sethero(hero).accept(ai);
 			}
 
+			if(node.turns == 0)
+			{
+				logAi->error(
+					"Enable to complete chain. Expected hero %s to arive to %s but he is at %s", 
+					hero.name, 
+					node.coord.toString(),
+					hero->visitablePos().toString());
+
+				return;
+			}
+
 			// no exception means we were not able to rich the tile
 			ai->nullkiller->lockHero(hero.get());
 			blockedIndexes.insert(node.parentIndex);

+ 194 - 117
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -190,17 +190,6 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf
 		{
 			dstNode->specialAction->applyOnDestination(dstNode->actor->hero, destination, source, dstNode, srcNode);
 		}
-
-#if AI_TRACE_LEVEL >= 2
-		logAi->trace(
-			"Commited %s -> %s, cost: %f, hero: %s, mask: %x, army: %i", 
-			source.coord.toString(),
-			destination.coord.toString(),
-			destination.cost,
-			dstNode->actor->toString(),
-			dstNode->actor->chainMask,
-			dstNode->actor->armyValue);
-#endif
 	});
 }
 
@@ -212,6 +201,11 @@ void AINodeStorage::commit(
 	int movementLeft, 
 	float cost) const
 {
+	if(destination->actor->chainMask == 195 && turn == 0)
+	{
+		throw std::exception();
+	}
+
 	destination->action = action;
 	destination->cost = cost;
 	destination->moveRemains = movementLeft;
@@ -221,6 +215,19 @@ void AINodeStorage::commit(
 	destination->danger = source->danger;
 	destination->theNodeBefore = source->theNodeBefore;
 	destination->chainOther = nullptr;
+
+#if AI_TRACE_LEVEL >= 2
+	logAi->trace(
+		"Commited %s -> %s, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld",
+		source->coord.toString(),
+		destination->coord.toString(),
+		destination->cost,
+		std::to_string(destination->turns),
+		destination->moveRemains,
+		destination->actor->toString(),
+		destination->actor->chainMask,
+		destination->actor->armyValue);
+#endif
 }
 
 std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
@@ -318,7 +325,7 @@ void AINodeStorage::calculateHeroChain(
 
 #if AI_TRACE_LEVEL >= 2
 		logAi->trace(
-			"Thy exchange %s[%i] -> %s[%i] at %s",
+			"Thy exchange %s[%x] -> %s[%x] at %s",
 			node->actor->toString(),
 			node->actor->chainMask,
 			srcNode->actor->toString(),
@@ -343,7 +350,7 @@ void AINodeStorage::calculateHeroChain(
 	{
 #if AI_TRACE_LEVEL >= 2
 		logAi->trace(
-			"Exchange allowed %s[%i] -> %s[%i] at %s",
+			"Exchange allowed %s[%x] -> %s[%x] at %s",
 			other->actor->toString(),
 			other->actor->chainMask,
 			carrier->actor->toString(),
@@ -423,12 +430,14 @@ void AINodeStorage::addHeroChain(const std::vector<ExchangeCandidate> & result)
 
 #if AI_TRACE_LEVEL >= 2
 		logAi->trace(
-			"Chain accepted at %s %s -> %s, mask %x, cost %f, army %i", 
+			"Chain accepted at %s %s -> %s, mask %x, cost %f, turn: %s, mp: %d, army %i", 
 			exchangeNode->coord.toString(), 
 			other->actor->toString(), 
 			exchangeNode->actor->toString(),
 			exchangeNode->actor->chainMask,
 			exchangeNode->cost,
+			std::to_string(exchangeNode->turns),
+			exchangeNode->moveRemains,
 			exchangeNode->actor->armyValue);
 #endif
 		heroChain.push_back(exchangeNode);
@@ -581,16 +590,110 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
 	return neighbours;
 }
 
-void AINodeStorage::getBestInitialNodeForTownPortal()
+struct TowmPortalFinder
 {
+	const std::vector<CGPathNode *> & initialNodes;
+	SecSkillLevel::SecSkillLevel townPortalSkillLevel;
+	uint64_t movementNeeded;
+	const ChainActor * actor;
+	const CGHeroInstance * hero;
+	std::vector<const CGTownInstance *> targetTowns;
+	AINodeStorage * nodeStorage;
 
-}
+	SpellID spellID;
+	const CSpell * townPortal;
+
+	TowmPortalFinder(
+		const ChainActor * actor,
+		const std::vector<CGPathNode *> & initialNodes,
+		std::vector<const CGTownInstance *> targetTowns,
+		AINodeStorage * nodeStorage)
+		:actor(actor), initialNodes(initialNodes), hero(actor->hero),
+		targetTowns(targetTowns), nodeStorage(nodeStorage)
+	{
+		spellID = SpellID::TOWN_PORTAL;
+		townPortal = spellID.toSpell();
+
+		// TODO: Copy/Paste from TownPortalMechanics
+		townPortalSkillLevel = SecSkillLevel::SecSkillLevel(hero->getSpellSchoolLevel(townPortal));
+		movementNeeded = GameConstants::BASE_MOVEMENT_COST * (townPortalSkillLevel >= SecSkillLevel::EXPERT ? 2 : 3);
+	}
+
+	bool actorCanCastTownPortal()
+	{
+		return hero->canCastThisSpell(townPortal) && hero->mana >= hero->getSpellCost(townPortal);
+	}
+
+	CGPathNode * getBestInitialNodeForTownPortal(const CGTownInstance * targetTown)
+	{
+		CGPathNode * bestNode = nullptr;
+
+		for(CGPathNode * node : initialNodes)
+		{
+			auto aiNode = nodeStorage->getAINode(node);
+
+			if(aiNode->actor->baseActor != actor
+				|| node->layer != EPathfindingLayer::LAND
+				|| node->moveRemains < movementNeeded)
+			{
+				continue;
+			}
+
+			if(townPortalSkillLevel < SecSkillLevel::ADVANCED)
+			{
+				const CGTownInstance * nearestTown = *vstd::minElementByFun(targetTowns, [&](const CGTownInstance * t) -> int
+				{
+					return node->coord.dist2dSQ(t->visitablePos());
+				});
+
+				if(targetTown != nearestTown)
+					continue;
+			}
+
+			if(!bestNode || bestNode->cost > node->cost)
+				bestNode = node;
+		}
+
+		return bestNode;
+	}
+
+	boost::optional<AIPathNode *> createTownPortalNode(const CGTownInstance * targetTown)
+	{
+		auto bestNode = getBestInitialNodeForTownPortal(targetTown);
+
+		if(!bestNode)
+			return boost::none;
+
+		auto nodeOptional = nodeStorage->getOrCreateNode(targetTown->visitablePos(), EPathfindingLayer::LAND, actor->castActor);
+
+		if(!nodeOptional)
+			return boost::none;
+
+		AIPathNode * node = nodeOptional.get();
+		float movementCost = (float)movementNeeded / (float)hero->maxMovePoints(EPathfindingLayer::LAND);
+
+		movementCost += bestNode->cost;
+
+		if(node->action == CGPathNode::UNKNOWN || node->cost > movementCost)
+		{
+			nodeStorage->commit(
+				node,
+				nodeStorage->getAINode(bestNode),
+				CGPathNode::TELEPORT_NORMAL,
+				bestNode->turns,
+				bestNode->moveRemains - movementNeeded,
+				movementCost);
+
+			node->theNodeBefore = bestNode;
+			node->specialAction.reset(new AIPathfinding::TownPortalAction(targetTown));
+		}
+
+		return nodeOptional;
+	}
+};
 
 void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *> & initialNodes)
 {
-	SpellID spellID = SpellID::TOWN_PORTAL;
-	const CSpell * townPortal = spellID.toSpell();
-
 	std::set<const ChainActor *> actorsOfInitial;
 
 	for(const CGPathNode * node : initialNodes)
@@ -605,92 +708,36 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
 		if(!actor->hero)
 			continue;
 
-		auto hero = actor->hero;
+		auto towns = cb->getTownsInfo(false);
 
-		if(hero->canCastThisSpell(townPortal) && hero->mana >= hero->getSpellCost(townPortal))
+		vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool
 		{
-			auto towns = cb->getTownsInfo(false);
-
-			vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool
-			{
-				return cb->getPlayerRelations(hero->tempOwner, t->tempOwner) == PlayerRelations::ENEMIES;
-			});
+			return cb->getPlayerRelations(actor->hero->tempOwner, t->tempOwner) == PlayerRelations::ENEMIES;
+		});
 
-			if(!towns.size())
-			{
-				return; // no towns no need to run loop further
-			}
+		if(!towns.size())
+		{
+			return; // no towns no need to run loop further
+		}
 
-			// TODO: Copy/Paste from TownPortalMechanics
-			auto skillLevel = hero->getSpellSchoolLevel(townPortal);
-			auto movementNeeded = GameConstants::BASE_MOVEMENT_COST * (skillLevel >= 3 ? 2 : 3);
+		TowmPortalFinder townPortalFinder(actor, initialNodes, towns, this);
 
+		if(townPortalFinder.actorCanCastTownPortal())
+		{
 			for(const CGTownInstance * targetTown : towns)
 			{
-				CGPathNode * bestNode = nullptr;
-
-				for(CGPathNode * node : initialNodes)
-				{
-					auto aiNode = getAINode(node);
-
-					if(aiNode->actor->baseActor != actor
-						|| node->layer != EPathfindingLayer::LAND
-						|| node->moveRemains < movementNeeded)
-					{
-						continue;
-					}
-
-					if(skillLevel < SecSkillLevel::ADVANCED)
-					{
-						const CGTownInstance * nearestTown = *vstd::minElementByFun(towns, [&](const CGTownInstance * t) -> int
-						{
-							return node->coord.dist2dSQ(t->visitablePos());
-						});
-
-						if(targetTown != nearestTown)
-							continue;
-					}
-
-					if(!bestNode || bestNode->cost > node->cost)
-						bestNode = node;
-				}
-
-				if(!bestNode)
-					continue;
-
 				// TODO: allow to hide visiting hero in garrison
-				if(targetTown->visitingHero && targetTown->visitingHero != hero)
+				if(targetTown->visitingHero && targetTown->visitingHero != actor->hero)
 					continue;
 
-				auto nodeOptional = getOrCreateNode(targetTown->visitablePos(), EPathfindingLayer::LAND, actor->castActor);
+				auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
 
 				if(nodeOptional)
 				{
-					float movementCost = (float)movementNeeded / (float)hero->maxMovePoints(EPathfindingLayer::LAND);
-
-					movementCost += bestNode->cost;
-
-#ifdef AI_TRACE_LEVEL >= 1
+#if AI_TRACE_LEVEL >= 1
 					logAi->trace("Adding town portal node at %s", targetTown->name);
 #endif
-
-					AIPathNode * node = nodeOptional.get();
-
-					if(node->action == CGPathNode::UNKNOWN || node->cost > movementCost)
-					{
-						commit(
-							node,
-							getAINode(bestNode),
-							CGPathNode::TELEPORT_NORMAL,
-							bestNode->turns,
-							bestNode->moveRemains - movementNeeded,
-							movementCost);
-
-						node->theNodeBefore = bestNode;
-						node->specialAction.reset(new AIPathfinding::TownPortalAction(targetTown));
-					}
-
-					initialNodes.push_back(node);
+					initialNodes.push_back(nodeOptional.get());
 				}
 			}
 		}
@@ -726,20 +773,21 @@ bool AINodeStorage::hasBetterChain(
 		{
 			if(node.cost < candidateNode->cost)
 			{
-#ifdef AI_TRACE_LEVEL >= 1
+#if AI_TRACE_LEVEL >= 2
 				logAi->trace(
-					"Block ineficient move %s:->%s, mask=%i, mp diff: %i",
+					"Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i",
 					source->coord.toString(),
 					candidateNode->coord.toString(),
+					candidateNode->actor->hero->name,
 					candidateNode->actor->chainMask,
+					candidateNode->actor->armyValue,
 					node.moveRemains - candidateNode->moveRemains);
 #endif
 				return true;
 			}
 		}
 
-		if(candidateActor->actorExchangeCount == 1
-			&& (candidateActor->chainMask & node.actor->chainMask) == 0)
+		if((candidateActor->chainMask & node.actor->chainMask) == 0)
 			continue;
 
 		auto nodeActor = node.actor;
@@ -749,15 +797,42 @@ bool AINodeStorage::hasBetterChain(
 		if(nodeArmyValue > candidateArmyValue
 			&& node.cost <= candidateNode->cost)
 		{
+#if AI_TRACE_LEVEL >= 2
+			logAi->trace(
+				"Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i",
+				source->coord.toString(),
+				candidateNode->coord.toString(),
+				candidateNode->actor->hero->name,
+				candidateNode->actor->chainMask,
+				candidateNode->actor->armyValue,
+				node.moveRemains - candidateNode->moveRemains);
+#endif
 			return true;
 		}
 
-		if(nodeArmyValue == candidateArmyValue
+		/*if(nodeArmyValue == candidateArmyValue
 			&& nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength
 			&& node.cost <= candidateNode->cost)
 		{
+			if(nodeActor->heroFightingStrength == candidateActor->heroFightingStrength
+				&& node.cost == candidateNode->cost
+				&& &node < candidateNode)
+			{
+				continue;
+			}
+
+#if AI_TRACE_LEVEL >= 2
+			logAi->trace(
+				"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i",
+				source->coord.toString(),
+				candidateNode->coord.toString(),
+				candidateNode->actor->hero->name,
+				candidateNode->actor->chainMask,
+				candidateNode->actor->armyValue,
+				node.moveRemains - candidateNode->moveRemains);
+#endif
 			return true;
-		}
+		}*/
 	}
 
 	return false;
@@ -821,11 +896,12 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa
 		if(node->chainOther)
 			fillChainInfo(node->chainOther, path, parentIndex);
 
-		if(node->actor->hero->visitablePos() != node->coord)
+		//if(node->actor->hero->visitablePos() != node->coord)
 		{
 			AIPathNodeInfo pathNode;
 			pathNode.cost = node->cost;
 			pathNode.targetHero = node->actor->hero;
+			pathNode.chainMask = node->actor->chainMask;
 			pathNode.specialAction = node->specialAction;
 			pathNode.turns = node->turns;
 			pathNode.danger = node->danger;
@@ -862,7 +938,7 @@ int3 AIPath::targetTile() const
 {
 	if(nodes.size())
 	{
-		return nodes.front().coord;
+		return targetNode().coord;
 	}
 
 	return int3(-1, -1, -1);
@@ -873,36 +949,35 @@ const AIPathNodeInfo & AIPath::firstNode() const
 	return nodes.back();
 }
 
+const AIPathNodeInfo & AIPath::targetNode() const
+{
+	auto & node = nodes.front();
+
+	return targetHero == node.targetHero ? node : nodes.at(1);
+}
+
 uint64_t AIPath::getPathDanger() const
 {
-	if(nodes.size())
-	{
-		return nodes.front().danger;
-	}
+	if(nodes.empty())
+		return 0;
 
-	return 0;
+	return targetNode().danger;
 }
 
 float AIPath::movementCost() const
 {
-	if(nodes.size())
-	{
-		return nodes.front().cost;
-	}
+	if(nodes.empty())
+		return 0.0f;
 
-	// TODO: boost:optional?
-	return 0.0;
+	return targetNode().cost;
 }
 
 uint8_t AIPath::turn() const
 {
-	if(nodes.size())
-	{
-		return nodes.front().turns;
-	}
+	if(nodes.empty())
+		return 0;
 
-	// TODO: boost:optional?
-	return 0;
+	return targetNode().turns;
 }
 
 uint64_t AIPath::getHeroStrength() const
@@ -921,9 +996,11 @@ uint64_t AIPath::getTotalDanger(HeroPtr hero) const
 std::string AIPath::toString()
 {
 	std::stringstream str;
-	
+
+	str << targetHero->name << "[" << std::hex << chainMask << std::dec << "]" << ": ";
+
 	for(auto node : nodes)
-		str << node.targetHero->name << "->" << node.coord.toString() << "; ";
+		str << node.targetHero->name << "[" << std::hex << node.chainMask << std::dec << "]" << "->" << node.coord.toString() << "; ";
 
 	return str.str();
 }

+ 23 - 10
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -10,8 +10,8 @@
 
 #pragma once
 
-#define VCMI_TRACE_PATHFINDER 2
-#define AI_TRACE_LEVEL 2
+#define VCMI_TRACE_PATHFINDER 1
+#define AI_TRACE_LEVEL 1
 
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/mapObjects/CGHeroInstance.h"
@@ -39,6 +39,7 @@ struct AIPathNodeInfo
 	uint64_t danger;
 	const CGHeroInstance * targetHero;
 	int parentIndex;
+	uint64_t chainMask;
 	std::shared_ptr<const ISpecialAction> specialAction;
 };
 
@@ -66,6 +67,8 @@ struct AIPath
 
 	const AIPathNodeInfo & firstNode() const;
 
+	const AIPathNodeInfo & targetNode() const;
+
 	float movementCost() const;
 
 	uint8_t turn() const;
@@ -99,7 +102,7 @@ private:
 
 public:
 	/// more than 1 chain layer for each hero allows us to have more than 1 path to each tile so we can chose more optimal one.
-	static const int NUM_CHAINS = 5 * GameConstants::MAX_HEROES_PER_PLAYER;
+	static const int NUM_CHAINS = 10 * GameConstants::MAX_HEROES_PER_PLAYER;
 	
 	AINodeStorage(const int3 & sizes);
 	~AINodeStorage();
@@ -120,11 +123,28 @@ public:
 
 	virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
 
+	void commit(
+		AIPathNode * destination,
+		const AIPathNode * source,
+		CGPathNode::ENodeAction action,
+		int turn,
+		int movementLeft,
+		float cost) const;
+
 	const AIPathNode * getAINode(const CGPathNode * node) const;
 	void updateAINode(CGPathNode * node, std::function<void (AIPathNode *)> updater);
 
 	bool hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const;
 
+	bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
+	{
+		// further chain distribution is calculated as the last stage
+		if(heroChainPass && destination.node->turns > heroChainTurn)
+			return true;
+
+		return hasBetterChain(source, destination);
+	}
+
 	template<class NodeRange>
 	bool hasBetterChain(
 		const CGPathNode * source, 
@@ -167,13 +187,6 @@ private:
 
 	void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
 	void fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const;
-	void commit(
-		AIPathNode * destination, 
-		const AIPathNode * source, 
-		CGPathNode::ENodeAction action, 
-		int turn, 
-		int movementLeft, 
-		float cost) const;
 
 	ExchangeCandidate calculateExchange(
 		ChainActor * exchangeActor, 

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -45,7 +45,7 @@ namespace AIPathfinding
 		const PathfinderConfig * pathfinderConfig,
 		CPathfinderHelper * pathfinderHelper) const
 	{
-		if(nodeStorage->hasBetterChain(source, destination))
+		if(nodeStorage->isMovementIneficient(source, destination))
 		{
 			destination.blocked = true;
 

+ 6 - 2
AI/Nullkiller/VCAI.cpp

@@ -32,7 +32,7 @@ extern FuzzyHelper * fh;
 
 class CGVisitableOPW;
 
-const double SAFE_ATTACK_CONSTANT = 1.5;
+const float SAFE_ATTACK_CONSTANT = 1.5;
 
 //one thread may be turn of AI and another will be handling a side effect for AI2
 boost::thread_specific_ptr<CCallback> cb;
@@ -1064,9 +1064,13 @@ void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
 		checkHeroArmy(h);
 		break;
 	case Obj::TOWN:
-		moveCreaturesToHero(dynamic_cast<const CGTownInstance *>(obj));
 		if(h->visitedTown) //we are inside, not just attacking
 		{
+			makePossibleUpgrades(h.get());
+
+			if(!h->visitedTown->garrisonHero)
+				moveCreaturesToHero(h->visitedTown);
+
 			townVisitsThisWeek[h].insert(h->visitedTown);
 			ah->updateHeroRoles();
 			if(ah->getHeroRole(h) == HeroRole::MAIN && !h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST)