Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove the AI unit growth bonuses and tune the income bonuses for different difficulty levels #8402

Merged
merged 73 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
866dcb6
Remove the AI unit growth bonuses and income bonuses for all difficul…
oleg-derevenetz Feb 13, 2024
cbc4c13
Tune the AI gold bonus
oleg-derevenetz Feb 15, 2024
4a7d3b3
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Feb 18, 2024
04be0fe
Tune the unit growth bonus
oleg-derevenetz Feb 18, 2024
8723dfe
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Feb 21, 2024
d098ba3
Remove the unit growth bonuses once again
oleg-derevenetz Feb 21, 2024
01aed5d
Add the infrastructure to apply the AI resource bonuses
oleg-derevenetz Feb 21, 2024
9dfdde7
Address the Clang-Tidy warning
oleg-derevenetz Feb 21, 2024
4b00fcb
Apply the IWYU suggestions
oleg-derevenetz Feb 21, 2024
5249aec
Implement the infrastructure for per-race growth bonuses
oleg-derevenetz Feb 21, 2024
7d69ce7
Disable the level 5 mage guilds on Easy difficulty
oleg-derevenetz Feb 21, 2024
93e7a5d
Add comment
oleg-derevenetz Feb 21, 2024
3da35bb
Tune the resource income
oleg-derevenetz Feb 25, 2024
3a3b55a
Rename the lambda
oleg-derevenetz Feb 25, 2024
4174a1f
Improve the defensive variant of the castle development a bit
oleg-derevenetz Mar 1, 2024
ff6fcfa
Apply IWYU suggestion
oleg-derevenetz Mar 1, 2024
d0ac6a8
Tune the AI income bonus
oleg-derevenetz Mar 1, 2024
af69614
Change the surrendering logic
oleg-derevenetz Mar 2, 2024
3ad7263
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 2, 2024
657dd7e
Simplify the logic
oleg-derevenetz Mar 2, 2024
49a5a70
Apply IWYU suggestions
oleg-derevenetz Mar 2, 2024
598da2c
Tune the surrender logic
oleg-derevenetz Mar 2, 2024
55ea609
Reduce the code duplication
oleg-derevenetz Mar 2, 2024
cfbaefd
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 6, 2024
7b4863d
Do not allow AI to surrender in the open field
oleg-derevenetz Mar 6, 2024
c814b99
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 13, 2024
1c3f408
Use sum of primary skills instead of hero's level
oleg-derevenetz Mar 13, 2024
2fb2a60
Remove extra semicolon
oleg-derevenetz Mar 13, 2024
87ccd2c
Remove a lot of (now) unused code
oleg-derevenetz Mar 13, 2024
53c2ab0
Update the copyright header
oleg-derevenetz Mar 13, 2024
656b4f1
Remove extra header
oleg-derevenetz Mar 13, 2024
f5eaf29
Fully re-evaluate the castles status at the end of the AI turn
oleg-derevenetz Mar 13, 2024
f2b472b
Style nits
oleg-derevenetz Mar 13, 2024
0080d6f
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 15, 2024
e493c35
Remove temporary variable
oleg-derevenetz Mar 16, 2024
1accb89
Remove extra parentheses
oleg-derevenetz Mar 16, 2024
3b79d88
Add castles-based bonus
oleg-derevenetz Mar 16, 2024
54564ec
Don't duplicate branches
oleg-derevenetz Mar 16, 2024
4ec35f1
Apply IWYU suggestions
oleg-derevenetz Mar 16, 2024
64fbcc8
Rename the lambda
oleg-derevenetz Mar 16, 2024
a889d0f
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 16, 2024
df18e01
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 18, 2024
4d3a8f6
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 20, 2024
0d383da
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 22, 2024
b42fb8f
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 26, 2024
20d727f
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 27, 2024
a283728
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Mar 31, 2024
0b5f55f
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Apr 4, 2024
0ee0448
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Apr 8, 2024
80b9fbf
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Apr 8, 2024
57381c6
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Apr 11, 2024
b7650e8
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Apr 24, 2024
0c0874a
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz Apr 28, 2024
90f9038
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 3, 2024
b44be30
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 5, 2024
0b9987f
Tune the bonus
oleg-derevenetz May 6, 2024
a48b0c3
Bone Dragons doesn't require resources to hire them
oleg-derevenetz May 6, 2024
c4867ba
Give the additional gold to Wizard & Warlock only if they have the le…
oleg-derevenetz May 7, 2024
44ba1e5
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 9, 2024
a3b378b
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 11, 2024
bb78d65
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 11, 2024
35fefbe
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 15, 2024
2ca241b
Apply IWYU suggestions
oleg-derevenetz May 15, 2024
f1ac4fd
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 16, 2024
a36d701
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 17, 2024
dee8e28
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 18, 2024
d085c19
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 19, 2024
c65ddde
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 22, 2024
54d5ca7
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 28, 2024
f3d733e
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 30, 2024
05a4961
Merge branch master into remove-ai-growth-income-bonuses
oleg-derevenetz May 30, 2024
0633eab
Tune the bonuses
oleg-derevenetz May 31, 2024
10e41fa
Tune the bonuses, update the logic a bit
oleg-derevenetz Jun 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/fheroes2/ai/ai.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 24 additions & 23 deletions src/fheroes2/ai/normal/ai_normal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<AICastle> getSortedCastleList( const VecCastles & castles, const std::set<int> & 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<AICastle> getSortedCastleList( const VecCastles & castles, const std::set<int> & castlesInDanger );

int getPriorityTarget( const HeroToMove & heroInfo, double & maxPriority );

double getGeneralObjectValue( const Heroes & hero, const int index, const double valueToIgnore, const uint32_t distanceToObject ) const;
Expand All @@ -360,17 +348,30 @@ namespace AI
std::set<int> 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.
// IMPORTANT!!! Do not call this method directly. Use other methods which call it internally.
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<IndexObject> _mapActionObjects;
std::map<int32_t, PriorityTask> _priorityTargets;
Expand Down
107 changes: 47 additions & 60 deletions src/fheroes2/ai/normal/ai_normal_battle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@

#include "ai.h"
#include "ai_normal.h"
#include "army.h"
#include "artifact.h"
#include "artifact_info.h"
#include "battle.h"
Expand Down Expand Up @@ -600,7 +599,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 ) {
Expand All @@ -620,98 +618,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, &currentUnit, &actions]() {
Expand Down
33 changes: 13 additions & 20 deletions src/fheroes2/ai/normal/ai_normal_castle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 );
}
}
Expand Down
20 changes: 13 additions & 7 deletions src/fheroes2/ai/normal/ai_normal_kingdom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -310,7 +310,6 @@ namespace AI
}

if ( buyArmy ) {
CastleTurn( castle, underThreat );
reinforceHeroInCastle( *recruit, castle, kingdom.GetFunds() );
}
else {
Expand Down Expand Up @@ -481,12 +480,16 @@ namespace AI
std::vector<AICastle> Normal::getSortedCastleList( const VecCastles & castles, const std::set<int> & castlesInDanger )
{
std::vector<AICastle> 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() );
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 );
ihhub marked this conversation as resolved.
Show resolved Hide resolved
sortedCastleList = getSortedCastleList( castles, castlesInDanger );
}

Expand All @@ -946,8 +952,8 @@ namespace AI
bool Normal::purchaseNewHeroes( const std::vector<AICastle> & sortedCastleList, const std::set<int> & 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;
Expand Down Expand Up @@ -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*/ )
Expand Down
Loading