diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index e2fb50dc7..d74ebbd8f 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -12,6 +12,10 @@ #include "../../lib/CStack.h" // TODO: remove // Eventually only IBattleInfoCallback and battle::Unit should be used, // CUnitState should be private and CStack should be removed completely +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/ObstacleCasterProxy.h" +#include "../../lib/battle/CObstacleInstance.h" uint64_t averageDmg(const DamageRange & range) { @@ -25,9 +29,57 @@ void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit damageCache[attacker->unitId()][defender->unitId()] = static_cast(damage) / attacker->getCount(); } +void DamageCache::buildObstacleDamageCache(std::shared_ptr hb, BattleSide side) +{ + for(const auto & obst : hb->battleGetAllObstacles(side)) + { + auto spellObstacle = dynamic_cast(obst.get()); + + if(!spellObstacle || !obst->triggersEffects()) + continue; + + auto triggerAbility = VLC->spells()->getById(obst->getTrigger()); + auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage(); + + if(!triggerIsNegative) + continue; + + const auto * hero = hb->battleGetFightingHero(spellObstacle->casterSide); + auto caster = spells::ObstacleCasterProxy(hb->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle); + + auto affectedHexes = obst->getAffectedTiles(); + auto stacks = hb->battleGetUnitsIf([](const battle::Unit * u) -> bool { + return u->alive() && !u->isTurret() && u->getPosition().isValid(); + }); + + for(auto stack : stacks) + { + std::shared_ptr inner = std::make_shared(hb->env, hb); + auto cast = spells::BattleCast(hb.get(), &caster, spells::Mode::PASSIVE, obst->getTrigger().toSpell()); + auto updated = inner->getForUpdate(stack->unitId()); + + spells::Target target; + target.push_back(spells::Destination(updated.get())); + + cast.castEval(inner->getServerCallback(), target); + + auto damageDealt = stack->getAvailableHealth() - updated->getAvailableHealth(); + + for(auto hex : affectedHexes) + { + obstacleDamage[hex][stack->unitId()] = damageDealt; + } + } + } +} void DamageCache::buildDamageCache(std::shared_ptr hb, BattleSide side) { + if(parent == nullptr) + { + buildObstacleDamageCache(hb, side); + } + auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool { return u->isValidTarget(); @@ -70,6 +122,23 @@ int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); } +int64_t DamageCache::getObstacleDamage(BattleHex hex, const battle::Unit * defender) +{ + if(parent) + return parent->getObstacleDamage(hex, defender); + + auto damages = obstacleDamage.find(hex); + + if(damages == obstacleDamage.end()) + return 0; + + auto damage = damages->second.find(defender->unitId()); + + return damage == damages->second.end() + ? 0 + : damage->second; +} + int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) { if(parent) @@ -288,6 +357,15 @@ AttackPossibility AttackPossibility::evaluate( { retaliatedUnits.push_back(attacker); } + + auto obstacleDamage = damageCache.getObstacleDamage(hex, attacker); + + if(obstacleDamage > 0) + { + ap.attackerDamageReduce += calculateDamageReduce(nullptr, attacker, obstacleDamage, damageCache, state); + + ap.attackerState->damage(obstacleDamage); + } } // ensure the defender is also affected diff --git a/AI/BattleAI/AttackPossibility.h b/AI/BattleAI/AttackPossibility.h index 990dcdb00..3ef8e1523 100644 --- a/AI/BattleAI/AttackPossibility.h +++ b/AI/BattleAI/AttackPossibility.h @@ -18,14 +18,18 @@ class DamageCache { private: std::unordered_map> damageCache; + std::map> obstacleDamage; DamageCache * parent; + void buildObstacleDamageCache(std::shared_ptr hb, BattleSide side); + public: DamageCache() : parent(nullptr) {} DamageCache(DamageCache * parent) : parent(parent) {} void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); int64_t getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); + int64_t getObstacleDamage(BattleHex hex, const battle::Unit * defender); int64_t getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); void buildDamageCache(std::shared_ptr hb, BattleSide side); }; diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 8ee042f14..555865619 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -226,11 +226,36 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) { return BattleAction::makeDefend(stack); } - else + + auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool + { + return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u); + }); + + bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4; + bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER + && !bestAttack.attack.shooting + && hb->battleGetFortifications().hasMoat + && !enemyMellee.empty() + && isTargetOutsideFort; + + if(siegeDefense) { - activeActionMade = true; - return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from); + logAi->trace("Evaluating exchange at %d self-defense", stack->getPosition().hex); + + BattleAttackInfo bai(stack, stack, 0, false); + AttackPossibility apDefend(stack->getPosition(), stack->getPosition(), bai); + + float defenseValue = scoreEvaluator.evaluateExchange(apDefend, 0, *targets, damageCache, hb); + + if((defenseValue > score && score <= 0) || (defenseValue > 2 * score && score > 0)) + { + return BattleAction::makeDefend(stack); + } } + + activeActionMade = true; + return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from); } } } diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 70c881d23..8726fda7e 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -28,6 +28,12 @@ float BattleExchangeVariant::trackAttack( std::shared_ptr hb, DamageCache & damageCache) { + if(!ap.attackerState) + { + logAi->trace("Skipping fake ap attack"); + return 0; + } + auto attacker = hb->getForUpdate(ap.attack.attacker->unitId()); float attackValue = ap.attackValue(); @@ -485,15 +491,18 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex)); } - for(auto hex : ap.attack.attacker->getHexes()) + if(!ap.attack.attacker->isTurret()) { - auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex); - for(auto unit : unitsReachingAttacker) + for(auto hex : ap.attack.attacker->getHexes()) { - if(unit->unitSide() != ap.attack.attacker->unitSide()) + auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex); + for(auto unit : unitsReachingAttacker) { - allReachableUnits.push_back(unit); - result.enemyUnitsReachingAttacker.insert(unit->unitId()); + if(unit->unitSide() != ap.attack.attacker->unitSide()) + { + allReachableUnits.push_back(unit); + result.enemyUnitsReachingAttacker.insert(unit->unitId()); + } } } } diff --git a/AI/BattleAI/PotentialTargets.cpp b/AI/BattleAI/PotentialTargets.cpp index a341921e6..f38415ef7 100644 --- a/AI/BattleAI/PotentialTargets.cpp +++ b/AI/BattleAI/PotentialTargets.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "PotentialTargets.h" #include "../../lib/CStack.h"//todo: remove +#include "../../lib/mapObjects/CGTownInstance.h" PotentialTargets::PotentialTargets( const battle::Unit * attacker, diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index c6a6c4d4e..02a333b7d 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -68,7 +68,12 @@ void RecruitHero::accept(AIGateway * ai) throw cannotFulfillGoalException("Town " + t->nodeName() + " is occupied. Cannot recruit hero!"); cb->recruitHero(t, heroToHire); - ai->nullkiller->heroManager->update(); + + { + std::unique_lock lockGuard(ai->nullkiller->aiStateMutex); + + ai->nullkiller->heroManager->update(); + } if(t->visitingHero) ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get());