Browse Source

Merge pull request #4989 from IvanSavenko/ai_fix

Fixes for unfinished items from AI pull request review
Ivan Savenko 11 months ago
parent
commit
99da0a15bb

+ 0 - 49
AI/BattleAI/BattleEvaluator.cpp

@@ -731,55 +731,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 
 					ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
 					ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
 				}
 				}
-				//! Some units may be dead alltogether. So if they existed before but not now, we know they were killed by the spell
-				for (const auto& unit : all)
-				{
-					if (!unit->isValidTarget())
-						continue;
-					bool isDead = true;
-					for (const auto& remainingUnit : allUnits)
-					{
-						if (remainingUnit->unitId() == unit->unitId())
-							isDead = false;
-					}
-					if (isDead)
-					{
-						auto newHealth = 0;
-						auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0);
-						if (oldHealth != newHealth)
-						{
-							auto damage = std::abs(oldHealth - newHealth);
-							auto originalDefender = cb->getBattle(battleID)->battleGetUnitByID(unit->unitId());
-							auto dpsReduce = AttackPossibility::calculateDamageReduce(
-								nullptr,
-								originalDefender && originalDefender->alive() ? originalDefender : unit,
-								damage,
-								innerCache,
-								state);
-							auto ourUnit = unit->unitSide() == side ? 1 : -1;
-							auto goodEffect = newHealth > oldHealth ? 1 : -1;
-							if (ourUnit * goodEffect == 1)
-							{
-								if (ourUnit && goodEffect && (unit->isClone() || unit->isGhost()))
-									continue;
-								ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
-							}
-							else
-								ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
-#if BATTLE_TRACE_LEVEL >= 1
-							logAi->trace(
-								"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
-								ps.spell->getNameTranslated(),
-								ps.dest.at(0).hexValue.hex,
-								unit->creatureId().toCreature()->getNameSingularTranslated(),
-								unit->getCount(),
-								dpsReduce,
-								oldHealth,
-								newHealth);
-#endif
-						}
-					}
-				}
 				for(const auto & unit : allUnits)
 				for(const auto & unit : allUnits)
 				{
 				{
 					if(!unit->isValidTarget(true))
 					if(!unit->isValidTarget(true))

+ 13 - 44
AI/Nullkiller/Engine/Settings.cpp

@@ -28,6 +28,8 @@ namespace NKAI
 		scoutHeroTurnDistanceLimit(5),
 		scoutHeroTurnDistanceLimit(5),
 		maxGoldPressure(0.3f), 
 		maxGoldPressure(0.3f), 
 		maxpass(10),
 		maxpass(10),
+		pathfinderBucketsCount(1),
+		pathfinderBucketSize(32),
 		allowObjectGraph(true),
 		allowObjectGraph(true),
 		useTroopsFromGarrisons(false),
 		useTroopsFromGarrisons(false),
 		openMap(true),
 		openMap(true),
@@ -35,49 +37,16 @@ namespace NKAI
 	{
 	{
 		JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
 		JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
 
 
-		if(node.Struct()["maxRoamingHeroes"].isNumber())
-		{
-			maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer();
-		}
-
-		if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber())
-		{
-			mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer();
-		}
-
-		if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber())
-		{
-			scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer();
-		}
-
-		if(node.Struct()["maxpass"].isNumber())
-		{
-			maxpass = node.Struct()["maxpass"].Integer();
-		}
-
-		if(node.Struct()["maxGoldPressure"].isNumber())
-		{
-			maxGoldPressure = node.Struct()["maxGoldPressure"].Float();
-		}
-
-		if(!node.Struct()["allowObjectGraph"].isNull())
-		{
-			allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
-		}
-
-		if(!node.Struct()["openMap"].isNull())
-		{
-			openMap = node.Struct()["openMap"].Bool();
-		}
-
-		if (!node.Struct()["useFuzzy"].isNull())
-		{
-			useFuzzy = node.Struct()["useFuzzy"].Bool();
-		}
-
-		if(!node.Struct()["useTroopsFromGarrisons"].isNull())
-		{
-			useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
-		}
+		maxRoamingHeroes = node["maxRoamingHeroes"].Integer();
+		mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer();
+		scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer();
+		maxpass = node["maxpass"].Integer();
+		pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer();
+		pathfinderBucketSize = node["pathfinderBucketSize"].Integer();
+		maxGoldPressure = node["maxGoldPressure"].Float();
+		allowObjectGraph = node["allowObjectGraph"].Bool();
+		openMap = node["openMap"].Bool();
+		useFuzzy = node["useFuzzy"].Bool();
+		useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
 	}
 	}
 }
 }

+ 4 - 0
AI/Nullkiller/Engine/Settings.h

@@ -25,6 +25,8 @@ namespace NKAI
 		int mainHeroTurnDistanceLimit;
 		int mainHeroTurnDistanceLimit;
 		int scoutHeroTurnDistanceLimit;
 		int scoutHeroTurnDistanceLimit;
 		int maxpass;
 		int maxpass;
+		int pathfinderBucketsCount;
+		int pathfinderBucketSize;
 		float maxGoldPressure;
 		float maxGoldPressure;
 		bool allowObjectGraph;
 		bool allowObjectGraph;
 		bool useTroopsFromGarrisons;
 		bool useTroopsFromGarrisons;
@@ -39,6 +41,8 @@ namespace NKAI
 		int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
 		int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
 		int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
 		int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
 		int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
 		int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
+		int getPathfinderBucketsCount() const { return pathfinderBucketsCount; }
+		int getPathfinderBucketSize() const { return pathfinderBucketSize; }
 		bool isObjectGraphAllowed() const { return allowObjectGraph; }
 		bool isObjectGraphAllowed() const { return allowObjectGraph; }
 		bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
 		bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
 		bool isOpenMap() const { return openMap; }
 		bool isOpenMap() const { return openMap; }

+ 19 - 9
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -39,17 +39,17 @@ const uint64_t CHAIN_MAX_DEPTH = 4;
 
 
 const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false;
 const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false;
 
 
-AISharedStorage::AISharedStorage(int3 sizes)
+AISharedStorage::AISharedStorage(int3 sizes, int numChains)
 {
 {
 	if(!shared){
 	if(!shared){
 		shared.reset(new boost::multi_array<AIPathNode, 4>(
 		shared.reset(new boost::multi_array<AIPathNode, 4>(
-			boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
+			boost::extents[sizes.z][sizes.x][sizes.y][numChains]));
 
 
 		nodes = shared;
 		nodes = shared;
 
 
 		foreach_tile_pos([&](const int3 & pos)
 		foreach_tile_pos([&](const int3 & pos)
 			{
 			{
-				for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++)
+				for(auto i = 0; i < numChains; i++)
 				{
 				{
 					auto & node = get(pos)[i];
 					auto & node = get(pos)[i];
 						
 						
@@ -92,8 +92,18 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
 	}
 	}
 }
 }
 
 
+int AINodeStorage::getBucketCount() const
+{
+	return ai->settings->getPathfinderBucketsCount();
+}
+
+int AINodeStorage::getBucketSize() const
+{
+	return ai->settings->getPathfinderBucketSize();
+}
+
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
-	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
+	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes, ai->settings->getPathfinderBucketSize() * ai->settings->getPathfinderBucketsCount())
 {
 {
 	accessibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
 	accessibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
 		boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
 		boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
@@ -169,8 +179,8 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 	const EPathfindingLayer layer, 
 	const EPathfindingLayer layer, 
 	const ChainActor * actor)
 	const ChainActor * actor)
 {
 {
-	int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % AIPathfinding::BUCKET_COUNT;
-	int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
+	int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % ai->settings->getPathfinderBucketsCount();
+	int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize();
 	auto chains = nodes.get(pos);
 	auto chains = nodes.get(pos);
 
 
 	if(blocked(pos, layer))
 	if(blocked(pos, layer))
@@ -178,7 +188,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 		return std::nullopt;
 		return std::nullopt;
 	}
 	}
 
 
-	for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--)
+	for(auto i = ai->settings->getPathfinderBucketSize() - 1; i >= 0; i--)
 	{
 	{
 		AIPathNode & node = chains[i + bucketOffset];
 		AIPathNode & node = chains[i + bucketOffset];
 
 
@@ -486,8 +496,8 @@ public:
 		AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
 		AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
 		:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
 		:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
 	{
 	{
-		existingChains.reserve(AIPathfinding::NUM_CHAINS);
-		newChains.reserve(AIPathfinding::NUM_CHAINS);
+		existingChains.reserve(storage.getBucketCount() * storage.getBucketSize());
+		newChains.reserve(storage.getBucketCount() * storage.getBucketSize());
 	}
 	}
 
 
 	void execute(const tbb::blocked_range<size_t>& r)
 	void execute(const tbb::blocked_range<size_t>& r)

+ 5 - 5
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -29,9 +29,6 @@ namespace NKAI
 {
 {
 namespace AIPathfinding
 namespace AIPathfinding
 {
 {
-	const int BUCKET_COUNT = 1;
-	const int BUCKET_SIZE = 32;
-	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
 	const int CHAIN_MAX_DEPTH = 4;
 	const int CHAIN_MAX_DEPTH = 4;
 }
 }
 
 
@@ -157,7 +154,7 @@ public:
 	static boost::mutex locker;
 	static boost::mutex locker;
 	static uint32_t version;
 	static uint32_t version;
 
 
-	AISharedStorage(int3 mapSize);
+	AISharedStorage(int3 sizes, int numChains);
 	~AISharedStorage();
 	~AISharedStorage();
 
 
 	STRONG_INLINE
 	STRONG_INLINE
@@ -197,6 +194,9 @@ public:
 	bool selectFirstActor();
 	bool selectFirstActor();
 	bool selectNextActor();
 	bool selectNextActor();
 
 
+	int getBucketCount() const;
+	int getBucketSize() const;
+
 	std::vector<CGPathNode *> getInitialNodes() override;
 	std::vector<CGPathNode *> getInitialNodes() override;
 
 
 	virtual void calculateNeighbours(
 	virtual void calculateNeighbours(
@@ -298,7 +298,7 @@ public:
 
 
 	inline int getBucket(const ChainActor * actor) const
 	inline int getBucket(const ChainActor * actor) const
 	{
 	{
-		return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
+		return ((uintptr_t)actor * 395) % getBucketCount();
 	}
 	}
 
 
 	void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
 	void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);

+ 1 - 1
client/lobby/SelectionTab.cpp

@@ -1112,6 +1112,6 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr<ElementInfo> info, bool
 		labelName->alignment = ETextAlignment::CENTER;
 		labelName->alignment = ETextAlignment::CENTER;
 		labelName->moveTo(Point(pos.x + LABEL_POS_X, labelName->pos.y));
 		labelName->moveTo(Point(pos.x + LABEL_POS_X, labelName->pos.y));
 	}
 	}
-	labelName->setText(info->getNameForList());
+	labelName->setText(info->name);
 	labelName->setColor(color);
 	labelName->setColor(color);
 }
 }

+ 4 - 1
config/ai/nkai/nkai-settings.json

@@ -6,5 +6,8 @@
 	"maxGoldPressure" : 0.3,
 	"maxGoldPressure" : 0.3,
 	"useTroopsFromGarrisons" : true,
 	"useTroopsFromGarrisons" : true,
 	"openMap": true,
 	"openMap": true,
-	"allowObjectGraph": false
+	"allowObjectGraph": false,
+	"pathfinderBucketsCount" : 1, // old value: 3,
+	"pathfinderBucketSize" : 32, // old value: 7,
+	"useFuzzy" : false
 }
 }

+ 14 - 0
config/gameConfig.json

@@ -488,6 +488,20 @@
 			// if enabled flying will work like in original game, otherwise nerf similar to HotA flying is applied
 			// if enabled flying will work like in original game, otherwise nerf similar to HotA flying is applied
 			"originalFlyRules" : true
 			"originalFlyRules" : true
 		},
 		},
+		
+		"resources" : {
+			// H3 mechanics - AI receives bonus (or malus, on easy) to his resource income
+			// AI will receive specified values as percentage of his weekly income
+			// So, "gems" : 200 will give AI player 200% of his daily income of gems over week, or, in other words,
+			// giving AI player 2 additional gems per week for every owned Gem Pond
+			"weeklyBonusesAI" : {
+				"pawn"  : { "gold" : -175 },
+				"knight": {},
+				"rook"  : {},
+				"queen" : { "wood" : 275 , "mercury" : 100, "ore" : 275, "sulfur" : 100, "crystal" : 100, "gems" : 100, "gold" : 175},
+				"king"  : { "wood" : 375 , "mercury" : 200, "ore" : 375, "sulfur" : 200, "crystal" : 200, "gems" : 200, "gold" : 350}
+			}
+		},
 
 
 		"spells":
 		"spells":
 		{
 		{

+ 8 - 0
config/schemas/gameSettings.json

@@ -133,6 +133,14 @@
 				"originalFlyRules" :        { "type" : "boolean" }
 				"originalFlyRules" :        { "type" : "boolean" }
 			}
 			}
 		},
 		},
+		"resources": {
+			"type" : "object",
+			"additionalProperties" : false,
+			"properties" : {
+				"weeklyBonusesAI" : { "type" : "object" }
+			}
+		},
+
 		"spells": {
 		"spells": {
 			"type" : "object",
 			"type" : "object",
 			"additionalProperties" : false,
 			"additionalProperties" : false,

+ 1 - 0
lib/GameSettings.cpp

@@ -89,6 +89,7 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE,           "pathfinder", "useMonolithOneWayUnique"             },
 		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE,           "pathfinder", "useMonolithOneWayUnique"             },
 		{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY,                  "pathfinder", "useMonolithTwoWay"                   },
 		{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY,                  "pathfinder", "useMonolithTwoWay"                   },
 		{EGameSettings::PATHFINDER_USE_WHIRLPOOL,                         "pathfinder", "useWhirlpool"                        },
 		{EGameSettings::PATHFINDER_USE_WHIRLPOOL,                         "pathfinder", "useWhirlpool"                        },
+		{EGameSettings::RESOURCES_WEEKLY_BONUSES_AI,                      "resources", "weeklyBonusesAI"                      },
 		{EGameSettings::TEXTS_ARTIFACT,                                   "textData",  "artifact"                             },
 		{EGameSettings::TEXTS_ARTIFACT,                                   "textData",  "artifact"                             },
 		{EGameSettings::TEXTS_CREATURE,                                   "textData",  "creature"                             },
 		{EGameSettings::TEXTS_CREATURE,                                   "textData",  "creature"                             },
 		{EGameSettings::TEXTS_FACTION,                                    "textData",  "faction"                              },
 		{EGameSettings::TEXTS_FACTION,                                    "textData",  "faction"                              },

+ 1 - 0
lib/IGameSettings.h

@@ -67,6 +67,7 @@ enum class EGameSettings
 	PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE,
 	PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE,
 	PATHFINDER_USE_MONOLITH_TWO_WAY,
 	PATHFINDER_USE_MONOLITH_TWO_WAY,
 	PATHFINDER_USE_WHIRLPOOL,
 	PATHFINDER_USE_WHIRLPOOL,
+	RESOURCES_WEEKLY_BONUSES_AI,
 	TEXTS_ARTIFACT,
 	TEXTS_ARTIFACT,
 	TEXTS_CREATURE,
 	TEXTS_CREATURE,
 	TEXTS_FACTION,
 	TEXTS_FACTION,

+ 14 - 37
server/processors/NewTurnProcessor.cpp

@@ -18,6 +18,7 @@
 #include "../../lib/IGameSettings.h"
 #include "../../lib/IGameSettings.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/TerrainHandler.h"
 #include "../../lib/TerrainHandler.h"
+#include "../../lib/constants/StringConstants.h"
 #include "../../lib/entities/building/CBuilding.h"
 #include "../../lib/entities/building/CBuilding.h"
 #include "../../lib/entities/faction/CTownHandler.h"
 #include "../../lib/entities/faction/CTownHandler.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/gameState/CGameState.h"
@@ -240,46 +241,22 @@ ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool ne
 	if (!state.isHuman())
 	if (!state.isHuman())
 	{
 	{
 		// Initialize bonuses for different resources
 		// Initialize bonuses for different resources
-		std::array<int, GameResID::COUNT> weeklyBonuses = {};
-
-		// Calculate weekly bonuses based on difficulty
-		if (gameHandler->gameState()->getStartInfo()->difficulty == 0)
-		{
-			weeklyBonuses[EGameResID::GOLD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GOLD] * (0.75 - 1) * 7));
-		}
-		else if (gameHandler->gameState()->getStartInfo()->difficulty == 3)
-		{
-			weeklyBonuses[EGameResID::GOLD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GOLD] * 0.25 * 7));
-			weeklyBonuses[EGameResID::WOOD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::WOOD] * 0.39 * 7));
-			weeklyBonuses[EGameResID::ORE] = static_cast<int>(std::round(incomeHandicapped[EGameResID::ORE] * 0.39 * 7));
-			weeklyBonuses[EGameResID::MERCURY] = static_cast<int>(std::round(incomeHandicapped[EGameResID::MERCURY] * 0.14 * 7));
-			weeklyBonuses[EGameResID::CRYSTAL] = static_cast<int>(std::round(incomeHandicapped[EGameResID::CRYSTAL] * 0.14 * 7));
-			weeklyBonuses[EGameResID::SULFUR] = static_cast<int>(std::round(incomeHandicapped[EGameResID::SULFUR] * 0.14 * 7));
-			weeklyBonuses[EGameResID::GEMS] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GEMS] * 0.14 * 7));
-		}
-		else if (gameHandler->gameState()->getStartInfo()->difficulty == 4)
-		{
-			weeklyBonuses[EGameResID::GOLD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GOLD] * 0.5 * 7));
-			weeklyBonuses[EGameResID::WOOD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::WOOD] * 0.53 * 7));
-			weeklyBonuses[EGameResID::ORE] = static_cast<int>(std::round(incomeHandicapped[EGameResID::ORE] * 0.53 * 7));
-			weeklyBonuses[EGameResID::MERCURY] = static_cast<int>(std::round(incomeHandicapped[EGameResID::MERCURY] * 0.28 * 7));
-			weeklyBonuses[EGameResID::CRYSTAL] = static_cast<int>(std::round(incomeHandicapped[EGameResID::CRYSTAL] * 0.28 * 7));
-			weeklyBonuses[EGameResID::SULFUR] = static_cast<int>(std::round(incomeHandicapped[EGameResID::SULFUR] * 0.28 * 7));
-			weeklyBonuses[EGameResID::GEMS] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GEMS] * 0.28 * 7));
-		}
+		int difficultyIndex = gameHandler->gameState()->getStartInfo()->difficulty;
+		const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyIndex];
+		const JsonNode & weeklyBonusesConfig = gameHandler->gameState()->getSettings().getValue(EGameSettings::RESOURCES_WEEKLY_BONUSES_AI);
+		const JsonNode & difficultyConfig = weeklyBonusesConfig[difficultyName];
 
 
 		// Distribute weekly bonuses over 7 days, depending on the current day of the week
 		// Distribute weekly bonuses over 7 days, depending on the current day of the week
-		for (int i = 0; i < GameResID::COUNT; ++i)
+		for (GameResID i : GameResID::ALL_RESOURCES())
 		{
 		{
-			int dailyBonus = weeklyBonuses[i] / 7;
-			int remainderBonus = weeklyBonuses[i] % 7;
-
-			// Apply the daily bonus for each day, and distribute the remainder accordingly
-			incomeHandicapped[static_cast<GameResID>(i)] += dailyBonus;
-			if (gameHandler->gameState()->getDate(Date::DAY_OF_WEEK) - 1 < remainderBonus)
-			{
-				incomeHandicapped[static_cast<GameResID>(i)] += 1;
-			}
+			const std::string & name = GameConstants::RESOURCE_NAMES[i];
+			int weeklyBonus = difficultyConfig[name].Integer();
+			int dayOfWeek = gameHandler->gameState()->getDate(Date::DAY_OF_WEEK);
+			int dailyIncome = incomeHandicapped[i];
+			int amountTillToday = dailyIncome * weeklyBonus * (dayOfWeek-1) / 7 / 100;
+			int amountAfterToday = dailyIncome * weeklyBonus * dayOfWeek / 7 / 100;
+			int dailyBonusToday = amountAfterToday - amountTillToday;
+			incomeHandicapped[static_cast<GameResID>(i)] += dailyBonusToday;
 		}
 		}
 	}
 	}
 
 

+ 1 - 1
test/googletest

@@ -1 +1 @@
-Subproject commit b796f7d44681514f58a683a3a71ff17c94edb0c1
+Subproject commit b514bdc898e2951020cbdca1304b75f5950d1f59