diff --git a/src/fheroes2/ai/ai.h b/src/fheroes2/ai/ai.h index 47c772ed839..9ed62d34b2d 100644 --- a/src/fheroes2/ai/ai.h +++ b/src/fheroes2/ai/ai.h @@ -105,6 +105,7 @@ namespace AI virtual void Reset(); virtual void resetPathfinder() = 0; + virtual bool isValidHeroObject( const Heroes & hero, const int32_t index, const bool underHero ) = 0; // Should be called at the beginning of the battle even if no AI-controlled players are diff --git a/src/fheroes2/ai/normal/ai_normal.h b/src/fheroes2/ai/normal/ai_normal.h index a6ef5bee4e8..9ca77ddb2a9 100644 --- a/src/fheroes2/ai/normal/ai_normal.h +++ b/src/fheroes2/ai/normal/ai_normal.h @@ -298,42 +298,30 @@ namespace AI void CastlePreBattle( Castle & castle ) override; - bool recruitHero( Castle & castle, bool buyArmy, bool underThreat ); - void reinforceHeroInCastle( Heroes & hero, Castle & castle, const Funds & budget ); - void evaluateRegionSafety(); - std::vector getSortedCastleList( const VecCastles & castles, const std::set & castlesInDanger ); - void resetPathfinder() override; - bool isValidHeroObject( const Heroes & hero, const int32_t index, const bool underHero ) override; void battleBegins() override; void tradingPostVisitEvent( Kingdom & kingdom ) override; + bool isValidHeroObject( const Heroes & hero, const int32_t index, const bool underHero ) override; + double getObjectValue( const Heroes & hero, const int index, const int objectType, const double valueToIgnore, const uint32_t distanceToObject ) const; double getTargetArmyStrength( const Maps::Tiles & tile, const MP2::MapObjectType objectType ); - bool isPriorityTask( const int32_t index ) const - { - return _priorityTargets.find( index ) != _priorityTargets.end(); - } - - bool isCriticalTask( const int32_t index ) const - { - const auto iter = _priorityTargets.find( index ); - if ( iter == _priorityTargets.end() ) { - return false; - } - - return iter->second.type == PriorityTaskType::ATTACK || iter->second.type == PriorityTaskType::DEFEND; - } - private: void CastleTurn( Castle & castle, const bool defensiveStrategy ); // Returns true if heroes can still do tasks but they have no move points. bool HeroesTurn( VecHeroes & heroes, const uint32_t startProgressValue, const uint32_t endProgressValue ); + bool recruitHero( Castle & castle, bool buyArmy ); + void reinforceHeroInCastle( Heroes & hero, Castle & castle, const Funds & budget ); + + void evaluateRegionSafety(); + + std::vector getSortedCastleList( const VecCastles & castles, const std::set & castlesInDanger ); + int getPriorityTarget( const HeroToMove & heroInfo, double & maxPriority ); double getGeneralObjectValue( const Heroes & hero, const int index, const double valueToIgnore, const uint32_t distanceToObject ) const; @@ -360,7 +348,6 @@ namespace AI std::set findCastlesInDanger( const Kingdom & kingdom ); void updatePriorityForEnemyArmy( const Kingdom & kingdom, const EnemyArmy & enemyArmy ); - void updatePriorityForCastle( const Castle & castle ); // Return true if the castle is in danger. @@ -368,9 +355,23 @@ namespace AI bool updateIndividualPriorityForCastle( const Castle & castle, const EnemyArmy & enemyArmy ); void removePriorityAttackTarget( const int32_t tileIndex ); - void updatePriorityAttackTarget( const Kingdom & kingdom, const Maps::Tiles & tile ); + bool isPriorityTask( const int32_t index ) const + { + return _priorityTargets.find( index ) != _priorityTargets.end(); + } + + bool isCriticalTask( const int32_t index ) const + { + const auto iter = _priorityTargets.find( index ); + if ( iter == _priorityTargets.end() ) { + return false; + } + + return iter->second.type == PriorityTaskType::ATTACK || iter->second.type == PriorityTaskType::DEFEND; + } + // The following member variables should not be saved or serialized std::vector _mapActionObjects; std::map _priorityTargets; diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 4f82dc7d2fa..94cc80504f6 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -39,7 +39,6 @@ #include "ai.h" #include "ai_normal.h" -#include "army.h" #include "artifact.h" #include "artifact_info.h" #include "battle.h" @@ -601,7 +600,6 @@ namespace AI } const int gameDifficulty = Game::getDifficulty(); - const bool isGameCampaign = Game::isCampaign(); // TODO: consider taking speed/turn order into account in the future if ( _myArmyStrength * Difficulty::getArmyStrengthRatioForAIRetreat( gameDifficulty ) >= _enemyArmyStrength ) { @@ -621,98 +619,87 @@ namespace AI const Kingdom & kingdom = actualHero->GetKingdom(); - const bool considerRetreat = [this, &arena, actualHero, gameDifficulty, isGameCampaign, hasValuableArtifacts, &kingdom]() { - if ( !Difficulty::allowAIToRetreat( gameDifficulty, isGameCampaign ) ) { + const bool isAbleToSurrender = [this, &arena, &kingdom]() { + if ( !arena.CanSurrenderOpponent( _myColor ) ) { return false; } - if ( !arena.CanRetreatOpponent( _myColor ) ) { - return false; - } + return kingdom.AllowPayment( { Resource::GOLD, arena.getForce( _myColor ).GetSurrenderCost() } ); + }(); - // If the hero has valuable artifacts, he should in any case consider retreating so that these artifacts do not end up at the disposal of the enemy, - // especially in the case of an alliance war - if ( hasValuableArtifacts ) { + const bool isPossibleToReHire = [actualHero, &kingdom]() { + // If the hero is not the last in his kingdom, then we will assume that there is a possibility of re-hiring - even if there are no castles in the + // kingdom, one of the remaining heroes can capture an enemy castle + const VecHeroes & heroes = kingdom.GetHeroes(); + if ( heroes.size() > 1 ) { return true; } - // Otherwise, if this hero is the last one, and the kingdom has no castles, then there is no point in retreating - if ( kingdom.GetHeroes().size() == 1 ) { - assert( kingdom.GetHeroes().at( 0 ) == actualHero ); - - if ( kingdom.GetCastles().empty() ) { - return false; - } - } + assert( heroes.size() == 1 && heroes.at( 0 ) == actualHero ); - // Otherwise, if this hero is relatively experienced, then he should think about retreating so that he can be hired again later - return actualHero->GetLevel() >= Difficulty::getMinHeroLevelForAIRetreat( gameDifficulty ); - }(); - - const bool considerSurrender = [this, &arena, actualHero, gameDifficulty, isGameCampaign, hasValuableArtifacts, &kingdom]() { - if ( !Difficulty::allowAIToSurrender( gameDifficulty, isGameCampaign ) ) { + // Otherwise, if this hero is the last one, and there are no castles in the kingdom, then it will be impossible to re-hire this hero + const VecCastles & castles = kingdom.GetCastles(); + if ( castles.empty() ) { return false; } - if ( !arena.CanSurrenderOpponent( _myColor ) ) { - return false; - } + // Otherwise, if this hero is defending the last castle, then it will be impossible to re-hire this hero + const Castle * castle = actualHero->inCastle(); + if ( castle && castles.size() == 1 ) { + assert( castles.at( 0 ) == castle ); - // If the hero has valuable artifacts, he should in any case consider surrendering so that these artifacts do not end up at the disposal of the enemy, - // especially in the case of an alliance war - if ( hasValuableArtifacts ) { - return true; + return false; } - // Otherwise, if this hero is the last one, and either the kingdom has no castles, or this hero is defending the last castle, then there is no point - // in surrendering - if ( kingdom.GetHeroes().size() == 1 ) { - assert( kingdom.GetHeroes().at( 0 ) == actualHero ); - - const VecCastles & castles = kingdom.GetCastles(); - if ( castles.empty() ) { - return false; - } + // Otherwise, assume that there is a possibility of re-hiring + return true; + }(); - const Castle * castle = actualHero->inCastle(); - if ( castle && castles.size() == 1 ) { - assert( castles.at( 0 ) == castle ); + const int minHeroTotalPrimarySkillLevelForRetreat = 10; - return false; - } + if ( !arena.CanRetreatOpponent( _myColor ) ) { + if ( !isAbleToSurrender ) { + return Outcome::ContinueBattle; } - // Otherwise, if this hero is relatively experienced, then he should think about surrendering so that he can be hired again later - return actualHero->GetLevel() >= Difficulty::getMinHeroLevelForAIRetreat( gameDifficulty ); - }(); - - const Force & force = arena.getForce( _myColor ); + // If the hero has valuable artifacts, he should surrender so that these artifacts do not end up at the disposal of the enemy, especially in the case + // of an alliance war + if ( hasValuableArtifacts ) { + return Outcome::Surrender; + } - if ( !considerRetreat ) { - if ( !considerSurrender ) { + // Otherwise, if this hero cannot be rehired, then there is no point in surrendering + if ( !isPossibleToReHire ) { return Outcome::ContinueBattle; } - if ( !kingdom.AllowPayment( { Resource::GOLD, force.GetSurrenderCost() } ) ) { - return Outcome::ContinueBattle; + // Otherwise, if this hero is relatively experienced, then he should surrender so that he can be hired again later + if ( actualHero->getTotalPrimarySkillLevel() >= minHeroTotalPrimarySkillLevelForRetreat ) { + return Outcome::Surrender; } - return Outcome::Surrender; + // Otherwise, there is no point in surrendering + return Outcome::ContinueBattle; } - if ( !considerSurrender ) { + // If the hero has valuable artifacts, he should retreat so that these artifacts do not end up at the disposal of the enemy, especially in the case of an + // alliance war + if ( hasValuableArtifacts ) { return Outcome::Retreat; } - if ( force.getStrengthOfArmyRemainingInCaseOfSurrender() < Army::getStrengthOfAverageStartingArmy( actualHero ) ) { - return Outcome::Retreat; + // Otherwise, if this hero cannot be rehired, then there is no point in retreating + if ( !isPossibleToReHire ) { + return Outcome::ContinueBattle; } - if ( !kingdom.AllowPayment( Funds{ Resource::GOLD, force.GetSurrenderCost() } * Difficulty::getGoldReserveRatioForAISurrender( gameDifficulty ) ) ) { + // Otherwise, if this hero is relatively experienced, then he should retreat so that he can be hired again later + if ( actualHero->getTotalPrimarySkillLevel() >= minHeroTotalPrimarySkillLevelForRetreat ) { return Outcome::Retreat; } - return Outcome::Surrender; + // Otherwise, there is no point in retreating or surrendering + return Outcome::ContinueBattle; }(); const auto farewellSpellcast = [this, &arena, ¤tUnit, &actions]() { diff --git a/src/fheroes2/ai/normal/ai_normal_castle.cpp b/src/fheroes2/ai/normal/ai_normal_castle.cpp index b2c3894da3d..a4ec4866134 100644 --- a/src/fheroes2/ai/normal/ai_normal_castle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_castle.cpp @@ -28,8 +28,6 @@ #include "ai.h" #include "ai_normal.h" #include "army.h" -#include "army_troop.h" -#include "battle_tower.h" #include "castle.h" #include "difficulty.h" #include "game.h" @@ -314,31 +312,26 @@ namespace AI void Normal::CastleTurn( Castle & castle, const bool defensiveStrategy ) { if ( defensiveStrategy ) { - // Avoid building monster dwellings when defensive as they might fall into enemy's hands, unless we have a lot of resources. - const Kingdom & kingdom = castle.GetKingdom(); + const Funds & kingdomFunds = castle.GetKingdom().GetFunds(); - // TODO: check if we can upgrade monsters. It is much cheaper (except Giants into Titans) to upgrade monsters than buy new ones. + // If the castle is potentially under threat, then it makes sense to try to hire the maximum number of troops so that the enemy cannot hire them even if he + // captures the castle, therefore, it is worth starting with hiring. + castle.recruitBestAvailable( kingdomFunds ); - Troops possibleReinforcement = castle.getAvailableArmy( kingdom.GetFunds() ); - double possibleReinforcementStrength = possibleReinforcement.GetStrength(); + Army & garrison = castle.GetArmy(); - // A very rough estimation of strength. We measure the strength of possible army to hire with the strength of purchasing a turret. - const Battle::Tower tower( castle, Battle::TowerType::TWR_RIGHT, 0 ); - const Troop towerMonster( Monster::ARCHER, tower.GetCount() ); - const double towerStrength = towerMonster.GetStrength(); - if ( possibleReinforcementStrength > towerStrength ) { - castle.recruitBestAvailable( kingdom.GetFunds() ); - OptimizeTroopsOrder( castle.GetArmy() ); - } + // Then we try to upgrade the existing units in the castle garrison... + garrison.UpgradeTroops( castle ); - if ( castle.GetActualArmy().getTotalCount() > 0 ) { - Build( castle, defensiveStructures ); - } + // ... and then we try to hire troops again, because after upgrading the existing troops, there could be a place for new units. + castle.recruitBestAvailable( kingdomFunds ); - castle.recruitBestAvailable( kingdom.GetFunds() ); - OptimizeTroopsOrder( castle.GetArmy() ); + OptimizeTroopsOrder( garrison ); + // Avoid building monster dwellings when defensive as they might fall into enemy's hands, unless we have a lot of resources. Instead, try to build defensive + // structures if there is at least some kind of garrison in the castle. if ( castle.GetActualArmy().getTotalCount() > 0 ) { + Build( castle, defensiveStructures ); Build( castle, supportingDefensiveStructures ); } } diff --git a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp index 5b978d1c9b2..ea18d96d5f3 100644 --- a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp +++ b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp @@ -269,7 +269,7 @@ namespace namespace AI { - bool Normal::recruitHero( Castle & castle, bool buyArmy, bool underThreat ) + bool Normal::recruitHero( Castle & castle, bool buyArmy ) { Kingdom & kingdom = castle.GetKingdom(); const Recruits & rec = kingdom.GetRecruits(); @@ -310,7 +310,6 @@ namespace AI } if ( buyArmy ) { - CastleTurn( castle, underThreat ); reinforceHeroInCastle( *recruit, castle, kingdom.GetFunds() ); } else { @@ -481,12 +480,16 @@ namespace AI std::vector Normal::getSortedCastleList( const VecCastles & castles, const std::set & castlesInDanger ) { std::vector sortedCastleList; + sortedCastleList.reserve( castles.size() ); + for ( Castle * castle : castles ) { - if ( !castle ) + if ( castle == nullptr ) { continue; + } const int32_t castleIndex = castle->GetIndex(); const uint32_t regionID = world.GetTiles( castleIndex ).GetRegion(); + sortedCastleList.emplace_back( castle, castlesInDanger.count( castleIndex ) > 0, _regions[regionID].safetyFactor, castle->getBuildingValue() ); } @@ -588,7 +591,8 @@ namespace AI const uint32_t threatDistanceLimit = 3000; const int32_t castleIndex = castle.GetIndex(); - // skip precise distance check if army is too far to be a threat + + // Skip precise distance check if army is too far to be a threat if ( Maps::GetApproximateDistance( enemyArmy.index, castleIndex ) * Maps::Ground::fastestMovePenalty > threatDistanceLimit ) { return false; } @@ -920,6 +924,8 @@ namespace AI // Sync the list of castles (if new ones were captured during the turn) if ( castles.size() != sortedCastleList.size() ) { evaluateRegionSafety(); + + castlesInDanger = findCastlesInDanger( kingdom ); sortedCastleList = getSortedCastleList( castles, castlesInDanger ); } @@ -946,8 +952,8 @@ namespace AI bool Normal::purchaseNewHeroes( const std::vector & sortedCastleList, const std::set & castlesInDanger, const int32_t availableHeroCount, const bool moreTasksForHeroes ) { - const bool slowEarlyGame = world.CountDay() < 5 && sortedCastleList.size() == 1; - const int32_t heroLimit = slowEarlyGame ? 2 : world.w() / Maps::SMALL + 2; + const bool isEarlyGameWithSingleCastle = world.CountDay() < 5 && sortedCastleList.size() == 1; + const int32_t heroLimit = isEarlyGameWithSingleCastle ? 2 : world.w() / Maps::SMALL + 2; if ( availableHeroCount >= heroLimit ) { return false; @@ -990,7 +996,7 @@ namespace AI } // target found, buy hero - return recruitmentCastle && recruitHero( *recruitmentCastle, !slowEarlyGame, false ); + return recruitmentCastle && recruitHero( *recruitmentCastle, !isEarlyGameWithSingleCastle ); } void Normal::tradingPostVisitEvent( Kingdom & /*kingdom*/ ) diff --git a/src/fheroes2/army/army.cpp b/src/fheroes2/army/army.cpp index 0ed496b9568..6cdb856b54e 100644 --- a/src/fheroes2/army/army.cpp +++ b/src/fheroes2/army/army.cpp @@ -24,7 +24,6 @@ #include "army.h" #include -#include #include #include #include @@ -239,17 +238,19 @@ Troops::~Troops() } ); } -void Troops::Assign( const Troop * it1, const Troop * it2 ) +void Troops::Assign( const Troop * itbeg, const Troop * itend ) { Clean(); iterator it = begin(); - while ( it != end() && it1 != it2 ) { - if ( it1->isValid() ) - ( *it )->Set( *it1 ); + while ( it != end() && itbeg != itend ) { + if ( itbeg->isValid() ) { + ( *it )->Set( *itbeg ); + } + ++it; - ++it1; + ++itbeg; } } @@ -281,10 +282,13 @@ void Troops::PushBack( const Monster & mons, uint32_t count ) void Troops::PopBack() { - if ( !empty() ) { - delete back(); - pop_back(); + if ( empty() ) { + return; } + + delete back(); + + pop_back(); } Troop * Troops::GetTroop( size_t pos ) @@ -1306,26 +1310,6 @@ double Army::GetStrength() const return result; } -double Army::getStrengthOfAverageStartingArmy( const Heroes * hero ) -{ - assert( hero != nullptr ); - - const int race = hero->GetRace(); - - double result = 0.0; - - for ( uint32_t dwelling : std::array{ DWELLING_MONSTER1, DWELLING_MONSTER2 } ) { - const Monster monster{ race, dwelling }; - assert( monster.isValid() ); - - const auto [min, max] = getNumberOfMonstersInStartingArmy( monster ); - - result += Troop{ monster, ( min + max ) / 2 }.GetStrength(); - } - - return result; -} - void Army::Reset( const bool defaultArmy /* = false */ ) { Troops::Clean(); diff --git a/src/fheroes2/army/army.h b/src/fheroes2/army/army.h index 041d77d800b..49e87431c1c 100644 --- a/src/fheroes2/army/army.h +++ b/src/fheroes2/army/army.h @@ -56,7 +56,7 @@ class Troops : protected std::vector Troops & operator=( const Troops & ) = delete; - void Assign( const Troop *, const Troop * ); + void Assign( const Troop * itbeg, const Troop * itend ); void Assign( const Troops & ); void Insert( const Troops & ); void PushBack( const Monster &, uint32_t ); @@ -173,9 +173,6 @@ class Army final : public Troops, public Control static NeutralMonsterJoiningCondition GetJoinSolution( const Heroes &, const Maps::Tiles &, const Troop & ); - // Returns the strength of the average starting army for a given hero (not taking into account the hero's bonuses) - static double getStrengthOfAverageStartingArmy( const Heroes * hero ); - static void drawSingleDetailedMonsterLine( const Troops & troops, int32_t cx, int32_t cy, int32_t width ); static void drawMultipleMonsterLines( const Troops & troops, int32_t posX, int32_t posY, int32_t lineWidth, bool isCompact, const bool isDetailedView, const bool isGarrisonView = false, const uint32_t thievesGuildsCount = 0 ); diff --git a/src/fheroes2/battle/battle_army.cpp b/src/fheroes2/battle/battle_army.cpp index 80026aa50e4..7bc3a45de9c 100644 --- a/src/fheroes2/battle/battle_army.cpp +++ b/src/fheroes2/battle/battle_army.cpp @@ -306,27 +306,3 @@ void Battle::Force::SyncArmyCount() troop->SetCount( unit->GetDead() > unit->GetInitialCount() ? 0 : unit->GetInitialCount() - unit->GetDead() ); } } - -double Battle::Force::getStrengthOfArmyRemainingInCaseOfSurrender() const -{ - double result = 0.0; - - // Consider only the state of the original army - for ( uint32_t index = 0; index < army.Size(); ++index ) { - const Troop * troop = army.GetTroop( index ); - if ( troop == nullptr || !troop->isValid() ) { - continue; - } - - const Unit * unit = FindUID( uids.at( index ) ); - if ( unit == nullptr ) { - continue; - } - - // Consider only the number of units that will remain in the army after the end of the battle (in particular, don't take into account the number of - // non-true-resurrected units) - result += Troop{ unit->GetMonster(), unit->GetDead() > unit->GetInitialCount() ? 0 : unit->GetInitialCount() - unit->GetDead() }.GetStrength(); - } - - return result; -} diff --git a/src/fheroes2/battle/battle_army.h b/src/fheroes2/battle/battle_army.h index d57f8b4f43e..b44b72b58a6 100644 --- a/src/fheroes2/battle/battle_army.h +++ b/src/fheroes2/battle/battle_army.h @@ -142,9 +142,6 @@ namespace Battle // Returns the cost of surrender (in units of gold) for the current army on the battlefield uint32_t GetSurrenderCost() const; - // Returns the strength of the army that will remain in case of surrender (not taking into account the hero's bonuses) - double getStrengthOfArmyRemainingInCaseOfSurrender() const; - Troops GetKilledTroops() const; bool animateIdleUnits() const; diff --git a/src/fheroes2/castle/castle.cpp b/src/fheroes2/castle/castle.cpp index c50f9aab54a..f8f3cbfda9d 100644 --- a/src/fheroes2/castle/castle.cpp +++ b/src/fheroes2/castle/castle.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #include #include @@ -957,62 +956,6 @@ void Castle::ActionNewWeek() } } -void Castle::ActionNewWeekAIBonuses() -{ - if ( world.GetWeekType().GetType() == WeekName::PLAGUE ) { - // No growth bonus can be applied. - return; - } - - if ( !isControlAI() ) { - // No AI - no perks! - return; - } - - if ( GetColor() == Color::NONE ) { - // Neutrals aren't considered as AI players. - return; - } - - static const std::array basicDwellings - = { DWELLING_MONSTER1, DWELLING_MONSTER2, DWELLING_MONSTER3, DWELLING_MONSTER4, DWELLING_MONSTER5, DWELLING_MONSTER6 }; - - for ( const building_t dwellingId : basicDwellings ) { - uint32_t * dwellingMonsters = GetDwelling( dwellingId ); - if ( dwellingMonsters == nullptr ) { - // Such dwelling (or its upgrade) has not been built. - continue; - } - - uint32_t originalGrowth = Monster( race, GetActualDwelling( dwellingId ) ).GetGrown(); - - if ( building & BUILD_WELL ) { - originalGrowth += GetGrownWell(); - } - - if ( ( dwellingId == DWELLING_MONSTER1 ) && ( building & BUILD_WEL2 ) ) { - originalGrowth += GetGrownWel2(); - } - - if ( originalGrowth == 0 ) { - continue; - } - - const long bonusGrowth = std::lround( originalGrowth * Difficulty::GetUnitGrowthBonusForAI( Game::getDifficulty(), Game::isCampaign(), dwellingId ) ); - if ( bonusGrowth >= 0 ) { - *dwellingMonsters += bonusGrowth; - - continue; - } - - // If the original unit growth is non-zero, then the total unit growth after the application of penalties should be at least one unit - const uint32_t growthPenalty = std::min( static_cast( -bonusGrowth ), originalGrowth - 1 ); - assert( *dwellingMonsters > growthPenalty ); - - *dwellingMonsters -= growthPenalty; - } -} - void Castle::ActionNewMonth() const { // Do nothing. @@ -2471,15 +2414,12 @@ void Castle::ActionAfterBattle( bool attacker_wins ) Castle * VecCastles::GetFirstCastle() const { - const_iterator it = std::find_if( begin(), end(), []( const Castle * castle ) { return castle->isCastle(); } ); - return end() != it ? *it : nullptr; -} + const_iterator iter = std::find_if( begin(), end(), []( const Castle * castle ) { return castle->isCastle(); } ); + if ( iter == end() ) { + return nullptr; + } -void VecCastles::ChangeColors( int col1, int col2 ) -{ - for ( iterator it = begin(); it != end(); ++it ) - if ( ( *it )->GetColor() == col1 ) - ( *it )->ChangeColor( col2 ); + return *iter; } AllCastles::AllCastles() diff --git a/src/fheroes2/castle/castle.h b/src/fheroes2/castle/castle.h index b4de495ee53..c163b0c64d7 100644 --- a/src/fheroes2/castle/castle.h +++ b/src/fheroes2/castle/castle.h @@ -238,10 +238,7 @@ class Castle : public MapPosition, public BitModes, public ColorBase, public Con void ChangeColor( int ); void ActionNewDay(); - void ActionNewWeek(); - void ActionNewWeekAIBonuses(); - void ActionNewMonth() const; void ActionPreBattle(); @@ -416,9 +413,14 @@ namespace CastleDialog struct VecCastles : public std::vector { - Castle * GetFirstCastle() const; + VecCastles() = default; + VecCastles( const VecCastles & ) = delete; + + ~VecCastles() = default; + + VecCastles & operator=( const VecCastles & ) = delete; - void ChangeColors( int, int ); + Castle * GetFirstCastle() const; }; class AllCastles @@ -457,11 +459,6 @@ class AllCastles std::for_each( _castles.begin(), _castles.end(), []( Castle * castle ) { castle->ActionNewWeek(); } ); } - void NewWeekAI() - { - std::for_each( _castles.begin(), _castles.end(), []( Castle * castle ) { castle->ActionNewWeekAIBonuses(); } ); - } - void NewMonth() { std::for_each( _castles.begin(), _castles.end(), []( const Castle * castle ) { castle->ActionNewMonth(); } ); diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index a786883f660..b2d3bbfb8df 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -23,9 +23,15 @@ #include "difficulty.h" +#include #include +#include +#include "kingdom.h" +#include "profit.h" +#include "race.h" #include "translations.h" +#include "world.h" std::string Difficulty::String( int difficulty ) { @@ -61,58 +67,110 @@ int Difficulty::GetScoutingBonusForAI( int difficulty ) default: break; } + return 0; } -double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) +Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const Kingdom & kingdom ) { + assert( kingdom.isControlAI() ); + + const auto getIncomeFromSetsOfResourceMines = []( const int resourceTypes, const uint32_t numOfSets ) { + Funds result; + + Resource::forEach( resourceTypes, [&result]( const int res ) { result += ProfitConditions::FromMine( res ); } ); + + return result * numOfSets; + }; + + const auto getBonusForCastles = [kingdomColor = kingdom.GetColor(), &kingdomCastles = kingdom.GetCastles()]() { + Funds result; + + const bool kingdomHasMarketplace = std::any_of( kingdomCastles.begin(), kingdomCastles.end(), []( const Castle * castle ) { + assert( castle != nullptr ); + + return castle->isBuild( BUILD_MARKETPLACE ); + } ); + + // Additional rare resources for hiring units from higher-level dwellings can only be provided if the kingdom already has some source of those resources - either + // through trade or through mining + const auto doesKingdomHaveResourceSource = [kingdomColor, kingdomHasMarketplace]( const int resourceType ) { + return ( kingdomHasMarketplace || world.CountCapturedMines( resourceType, kingdomColor ) > 0 ); + }; + + for ( const Castle * castle : kingdomCastles ) { + assert( castle != nullptr ); + + // AI at higher difficulty levels should be able to fully redeem the weekly unit growth in its castles + result += ProfitConditions::FromMine( Resource::GOLD ); + + // Provide additional resources only if there are higher-level dwellings in the castle to avoid distortions in the castle's development rate + if ( !castle->isBuild( DWELLING_MONSTER6 ) ) { + continue; + } + + switch ( castle->GetRace() ) { + case Race::KNGT: + case Race::NECR: + // Rare resources are not required to hire maximum-level units in these castles + break; + case Race::BARB: + if ( doesKingdomHaveResourceSource( Resource::CRYSTAL ) ) { + result += ProfitConditions::FromMine( Resource::CRYSTAL ); + } + break; + case Race::SORC: + if ( doesKingdomHaveResourceSource( Resource::MERCURY ) ) { + result += ProfitConditions::FromMine( Resource::MERCURY ); + } + break; + case Race::WRLK: + if ( doesKingdomHaveResourceSource( Resource::SULFUR ) ) { + result += ProfitConditions::FromMine( Resource::SULFUR ); + } + // The maximum level units in this castle are more expensive than in others + result += ProfitConditions::FromMine( Resource::GOLD ); + break; + case Race::WZRD: + if ( doesKingdomHaveResourceSource( Resource::GEMS ) ) { + result += ProfitConditions::FromMine( Resource::GEMS ); + } + // The maximum level units in this castle are more expensive than in others + result += ProfitConditions::FromMine( Resource::GOLD ); + break; + default: + assert( 0 ); + break; + } + } + + return result; + }; + switch ( difficulty ) { - case Difficulty::EASY: - // It is deduction from the income. - return -0.25; case Difficulty::HARD: - return 0.29; + return getIncomeFromSetsOfResourceMines( Resource::GOLD, 1 ); case Difficulty::EXPERT: - return 0.45; + return getIncomeFromSetsOfResourceMines( Resource::GOLD, 1 ) + getBonusForCastles(); case Difficulty::IMPOSSIBLE: - return 0.6; + return getIncomeFromSetsOfResourceMines( Resource::GOLD, 2 ) + getBonusForCastles(); default: break; } - return 0; + + return {}; } -double Difficulty::GetUnitGrowthBonusForAI( const int difficulty, const bool /* isCampaign */, const building_t /* dwelling */ ) +double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) { - // In the original game AI has a cheeky monster growth bonus depending on difficulty: - // Easy - 0.0 (no bonus) - // Normal - 0.0 (no bonus) - // Hard - 0.20 (or 20% extra) - // Expert - 0.32 (or 32% extra) - // Impossible - 0.44 (or 44% extra) - // This bonus was introduced to compensate weak AI in the game. - // - // However, with introduction of proper AI in this engine AI has become much stronger and some maps are impossible to beat. - // Also this bonus can be abused by players while capturing AI castles on a first day of a week. - // - // Completely removing these bonuses might break some maps and they become unplayable. - // Therefore, these bonuses are reduced by 5% which is the value of noise in many processes / systems. - switch ( difficulty ) { case Difficulty::EASY: - case Difficulty::NORMAL: - return 0; - case Difficulty::HARD: - return 0.14; - case Difficulty::EXPERT: - return 0.254; - case Difficulty::IMPOSSIBLE: - return 0.368; + // It is deduction from the income. + return -0.25; default: - // Did you add a new difficulty level? Add the logic above! - assert( 0 ); break; } + return 0; } @@ -125,22 +183,8 @@ int Difficulty::GetHeroMovementBonusForAI( int difficulty ) default: break; } - return 0; -} -bool Difficulty::allowAIToRetreat( const int /* difficulty */, const bool /* isCampaign */ ) -{ - return true; -} - -bool Difficulty::allowAIToSurrender( const int /* difficulty */, const bool /* isCampaign */ ) -{ - return true; -} - -int Difficulty::getMinHeroLevelForAIRetreat( const int /* difficulty */ ) -{ - return 3; + return 0; } double Difficulty::getArmyStrengthRatioForAIRetreat( const int difficulty ) @@ -156,12 +200,8 @@ double Difficulty::getArmyStrengthRatioForAIRetreat( const int difficulty ) default: break; } - return 100.0 / 6.0; -} -uint32_t Difficulty::getGoldReserveRatioForAISurrender( const int /* difficulty */ ) -{ - return 10; + return 100.0 / 6.0; } uint32_t Difficulty::GetDimensionDoorLimitForAI( int difficulty ) @@ -176,6 +216,7 @@ uint32_t Difficulty::GetDimensionDoorLimitForAI( int difficulty ) default: break; } + return UINT32_MAX; } @@ -227,6 +268,7 @@ bool Difficulty::allowAIToSplitWeakStacks( const int difficulty ) default: break; } + return true; } @@ -238,6 +280,7 @@ bool Difficulty::allowAIToDevelopCastlesOnDay( const int difficulty, const bool default: break; } + return true; } @@ -246,9 +289,10 @@ bool Difficulty::allowAIToBuildCastleBuilding( const int difficulty, const bool switch ( difficulty ) { case Difficulty::EASY: // Only the construction of the corresponding dwelling is limited, but not its upgrade - return isCampaign || building != DWELLING_MONSTER6; + return isCampaign || ( building != DWELLING_MONSTER6 && building != BUILD_MAGEGUILD5 ); default: break; } + return true; } diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index c6ec0dee3a9..93daa9967f1 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -27,6 +27,9 @@ #include #include "castle.h" +#include "resource.h" + +class Kingdom; namespace Difficulty { @@ -45,26 +48,19 @@ namespace Difficulty int GetScoutingBonusForAI( int difficulty ); - // Returns an extra gold bonus modifier for AI based on difficulty level. - double getGoldIncomeBonusForAI( const int difficulty ); + // Returns an extra resource bonus for AI based on difficulty level. + Funds getResourceIncomeBonusForAI( const int difficulty, const Kingdom & kingdom ); - // Returns an extra growth bonus modifier for AI based on difficulty level. - double GetUnitGrowthBonusForAI( const int difficulty, const bool isCampaign, const building_t dwelling ); + // Returns an extra gold bonus modifier for AI based on difficulty level. This modifier is applied after applying the resource income bonus. + double getGoldIncomeBonusForAI( const int difficulty ); int GetHeroMovementBonusForAI( int difficulty ); - bool allowAIToRetreat( const int difficulty, const bool isCampaign ); - bool allowAIToSurrender( const int difficulty, const bool isCampaign ); - - // Returns the minimum hero level at which the AI can consider the possibility of surrender or retreat from the battlefield for this hero - int getMinHeroLevelForAIRetreat( const int difficulty ); - // Returns the ratio of the strength of the enemy army to the strength of the AI army, above which the AI decides to surrender or retreat from the battlefield double getArmyStrengthRatioForAIRetreat( const int difficulty ); - // Returns the minimum ratio of the AI kingdom's gold reserve to the cost of surrender, at which the AI will prefer surrender to retreat from the battlefield - uint32_t getGoldReserveRatioForAISurrender( const int difficulty ); - + // Returns the limit on the number of times the Dimension Door spell can be cast, which is applied to each of the AI-controlled heroes individually during one AI + // turn uint32_t GetDimensionDoorLimitForAI( int difficulty ); bool areAIHeroRolesAllowed( const int difficulty ); diff --git a/src/fheroes2/game/game_startgame.cpp b/src/fheroes2/game/game_startgame.cpp index 33f4e4df930..522161f05f0 100644 --- a/src/fheroes2/game/game_startgame.cpp +++ b/src/fheroes2/game/game_startgame.cpp @@ -751,10 +751,6 @@ fheroes2::GameMode Interface::AdventureMap::StartGame() res = fheroes2::GameMode::END_TURN; - // All bonuses for AI must be applied on the first AI player turn, not the first player in general. - // This prevents human players from abusing AI bonuses. - bool applyAIBonuses = true; - for ( const Player * player : sortedPlayers ) { assert( player != nullptr ); @@ -830,12 +826,6 @@ fheroes2::GameMode Interface::AdventureMap::StartGame() // TODO: remove this temporary assertion assert( res == fheroes2::GameMode::END_TURN ); - if ( applyAIBonuses ) { - world.NewDayAI(); - - applyAIBonuses = false; - } - Cursor::Get().SetThemes( Cursor::WAIT ); conf.SetCurrentColor( playerColor ); diff --git a/src/fheroes2/heroes/heroes.cpp b/src/fheroes2/heroes/heroes.cpp index 8c65a8d3186..496897bc28c 100644 --- a/src/fheroes2/heroes/heroes.cpp +++ b/src/fheroes2/heroes/heroes.cpp @@ -879,7 +879,7 @@ int Heroes::GetManaIndexSprite() const int Heroes::getStatsValue() const { // experience and artifacts don't matter here, only natural stats - return attack + defense + power + knowledge + secondary_skills.GetTotalLevel(); + return getTotalPrimarySkillLevel() + secondary_skills.GetTotalLevel(); } double Heroes::getRecruitValue() const diff --git a/src/fheroes2/heroes/skill.h b/src/fheroes2/heroes/skill.h index 581ee913d51..c558b9d6077 100644 --- a/src/fheroes2/heroes/skill.h +++ b/src/fheroes2/heroes/skill.h @@ -186,6 +186,12 @@ namespace Skill int LevelUp( int race, int level, uint32_t seed ); + // Returns the sum of the values of the four primary skills (attack, defense, power and knowledge), belonging directly to the hero (i.e. excluding artifacts) + int getTotalPrimarySkillLevel() const + { + return attack + defense + power + knowledge; + } + static const char * String( const int skillType ); static std::string StringDescription( int, const Heroes * ); static int GetInitialSpell( int race ); diff --git a/src/fheroes2/kingdom/kingdom.cpp b/src/fheroes2/kingdom/kingdom.cpp index 9bb682b87d6..27f25f88611 100644 --- a/src/fheroes2/kingdom/kingdom.cpp +++ b/src/fheroes2/kingdom/kingdom.cpp @@ -83,7 +83,9 @@ namespace Funds getHandicapDependentIncome( const Funds & original, const Player::HandicapStatus handicapStatus ) { const int32_t handicapPercentage = getHandicapIncomePercentage( handicapStatus ); + Funds corrected( original ); + corrected.wood = std::min( corrected.wood, ( corrected.wood * handicapPercentage + 99 ) / 100 ); corrected.mercury = std::min( corrected.mercury, ( corrected.mercury * handicapPercentage + 99 ) / 100 ); corrected.ore = std::min( corrected.ore, ( corrected.ore * handicapPercentage + 99 ) / 100 ); @@ -176,25 +178,30 @@ bool Kingdom::isPlay() const void Kingdom::LossPostActions() { - if ( isPlay() ) { - Players::SetPlayerInGame( color, false ); + if ( !isPlay() ) { + return; + } - // Heroes::Dismiss() calls Kingdom::RemoveHero(), which eventually calls heroes.erase() - while ( !heroes.empty() ) { - Heroes * hero = heroes.back(); + Players::SetPlayerInGame( color, false ); - assert( hero->GetColor() == GetColor() ); + // Heroes::Dismiss() calls Kingdom::RemoveHero(), which eventually calls heroes.erase() + while ( !heroes.empty() ) { + Heroes * hero = heroes.back(); - hero->Dismiss( static_cast( Battle::RESULT_LOSS ) ); - } + assert( hero->GetColor() == GetColor() ); - if ( !castles.empty() ) { - castles.ChangeColors( GetColor(), Color::NONE ); - castles.clear(); - } + hero->Dismiss( static_cast( Battle::RESULT_LOSS ) ); + } + + for ( Castle * castle : castles ) { + assert( castle != nullptr && castle->GetColor() == GetColor() ); - world.ResetCapturedObjects( GetColor() ); + castle->ChangeColor( Color::NONE ); } + + castles.clear(); + + world.ResetCapturedObjects( GetColor() ); } void Kingdom::ActionBeforeTurn() @@ -596,20 +603,22 @@ bool Kingdom::AllowRecruitHero( bool check_payment ) const void Kingdom::ApplyPlayWithStartingHero() { - if ( !isPlay() || castles.empty() ) + if ( !isPlay() || castles.empty() ) { return; + } bool foundHeroes = false; for ( const Castle * castle : castles ) { - if ( castle == nullptr ) + if ( castle == nullptr ) { continue; + } - // check manual set hero (castle position + point(0, 1))? + // Check if there is a hero placed by the map creator near the castle entrance (castle position + point(0, 1)) const fheroes2::Point & cp = castle->GetCenter(); Heroes * hero = world.GetTiles( cp.x, cp.y + 1 ).getHero(); - // and move manual set hero to castle + // If there is, move it to the castle if ( hero && hero->GetColor() == GetColor() ) { const bool patrol = hero->Modes( Heroes::PATROL ); if ( hero->isValid() ) { @@ -624,19 +633,21 @@ void Kingdom::ApplyPlayWithStartingHero() hero->SetModes( Heroes::PATROL ); hero->SetPatrolCenter( cp ); } + foundHeroes = true; } } if ( !foundHeroes && Settings::Get().getCurrentMapInfo().startWithHeroInEachCastle ) { - // get first castle const Castle * first = castles.GetFirstCastle(); - if ( nullptr == first ) + if ( first == nullptr ) { first = castles.front(); + } Heroes * hero = world.GetHeroForHire( first->GetRace() ); - if ( hero && AllowRecruitHero( false ) ) + if ( hero && AllowRecruitHero( false ) ) { hero->Recruit( *first ); + } } } @@ -645,7 +656,7 @@ uint32_t Kingdom::GetMaxHeroes() return GameStatic::GetKingdomMaxHeroes(); } -Funds Kingdom::GetIncome( int type /* INCOME_ALL */ ) const +Funds Kingdom::GetIncome( int type /* = INCOME_ALL */ ) const { Funds totalIncome; @@ -704,15 +715,28 @@ Funds Kingdom::GetIncome( int type /* INCOME_ALL */ ) const } } - if ( isControlAI() && totalIncome.gold > 0 ) { - const int32_t bonusGold = static_cast( totalIncome.gold * Difficulty::getGoldIncomeBonusForAI( Game::getDifficulty() ) ); + if ( isControlAI() ) { + const Funds incomeBonus = Difficulty::getResourceIncomeBonusForAI( Game::getDifficulty(), *this ); + if ( incomeBonus.GetValidItemsCount() != 0 ) { + DEBUG_LOG( DBG_AI, DBG_TRACE, "AI bonus to the resource income has been applied to " << Color::String( color ) << ": " << incomeBonus.String() ); - totalIncome.gold += bonusGold; + totalIncome += incomeBonus; + } + + const int32_t goldBonus = static_cast( totalIncome.gold * Difficulty::getGoldIncomeBonusForAI( Game::getDifficulty() ) ); + if ( goldBonus != 0 ) { + DEBUG_LOG( DBG_AI, DBG_TRACE, + "AI bonus to the gold income has been applied to " << Color::String( color ) << ", original income: " << totalIncome.gold + << ", bonus income: " << goldBonus ); + + totalIncome.gold += goldBonus; + } } - // Some human players can have handicap for resources. const Player * player = Players::Get( color ); assert( player != nullptr ); + + // Some human players can have handicap for resources. return getHandicapDependentIncome( totalIncome, player->getHandicapStatus() ); } diff --git a/src/fheroes2/kingdom/kingdom.h b/src/fheroes2/kingdom/kingdom.h index ffa1c1087da..619b7c283c9 100644 --- a/src/fheroes2/kingdom/kingdom.h +++ b/src/fheroes2/kingdom/kingdom.h @@ -111,6 +111,7 @@ class Kingdom : public BitModes, public Control { return resource; } + Funds GetIncome( int type = INCOME_ALL ) const; double GetArmiesStrength() const; diff --git a/src/fheroes2/world/world.cpp b/src/fheroes2/world/world.cpp index f4fa297aa44..74fba204cb9 100644 --- a/src/fheroes2/world/world.cpp +++ b/src/fheroes2/world/world.cpp @@ -555,13 +555,6 @@ void World::NewDay() vec_eventsday.remove_if( [this]( const EventDate & v ) { return v.isDeprecated( day - 1 ); } ); } -void World::NewDayAI() -{ - if ( BeginWeek() ) { - vec_castles.NewWeekAI(); - } -} - void World::NewWeek() { // update objects diff --git a/src/fheroes2/world/world.h b/src/fheroes2/world/world.h index eb33f33515b..2533f36254a 100644 --- a/src/fheroes2/world/world.h +++ b/src/fheroes2/world/world.h @@ -314,7 +314,6 @@ class World : protected fheroes2::Size std::string DateString() const; void NewDay(); - void NewDayAI(); void NewWeek(); void NewMonth();