diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index f5047f9893..ded010f42a 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -91,6 +91,7 @@ void GlobalGameSettings::registerAllAddons() AddonMoreAnimals, AddonNoAlliedPush, AddonNoCoinsDefault, + AddonSingleSoldierCoinTraining, AddonNumScoutsExploration, AddonPeacefulMode, AddonRefundMaterials, diff --git a/libs/s25main/addons/AddonSingleSoldierCoinTraining.h b/libs/s25main/addons/AddonSingleSoldierCoinTraining.h new file mode 100644 index 0000000000..e8f4080297 --- /dev/null +++ b/libs/s25main/addons/AddonSingleSoldierCoinTraining.h @@ -0,0 +1,17 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "AddonBool.h" +#include "mygettext/mygettext.h" + +class AddonSingleSoldierCoinTraining : public AddonBool +{ +public: + AddonSingleSoldierCoinTraining() + : AddonBool(AddonId::SINGLE_SOLDIER_COIN_TRAINING, AddonGroup::Military, _("Coins train only one soldier"), + _("Gold coins promote only one lowest-rank soldier instead of all eligible soldiers.")) + {} +}; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 96b3203ff7..e4cea19830 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -28,6 +28,7 @@ #include "addons/AddonDefenderBehavior.h" #include "addons/AddonNoCoinsDefault.h" +#include "addons/AddonSingleSoldierCoinTraining.h" #include "addons/AddonAdjustMilitaryStrength.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index f7ddf54f08..da598bf301 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -81,6 +81,8 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x FORESTER_FARM_FIELD_AVOIDANCE = 0x01100000, + SINGLE_SOLDIER_COIN_TRAINING = 0x01100002, + FORESTER_REACH_RADIUS = 0x01200000, WOODCUTTER_REACH_RADIUS = 0x01200001, STONEMASON_REACH_RADIUS = 0x01200002) //-V:AddonId:801 diff --git a/libs/s25main/buildings/nobMilitary.cpp b/libs/s25main/buildings/nobMilitary.cpp index 47c8a7b05e..d3a44f1cf4 100644 --- a/libs/s25main/buildings/nobMilitary.cpp +++ b/libs/s25main/buildings/nobMilitary.cpp @@ -36,6 +36,7 @@ #include "gameData/MilitaryConsts.h" #include "gameData/SettingTypeConv.h" #include "s25util/Log.h" +#include #include #include @@ -315,28 +316,40 @@ void nobMilitary::HandleEvent(const unsigned id) { upgrade_event = nullptr; - // Soldaten befördern - // Von hinten durchgehen - // Wenn der nachfolgende (schwächere) Soldat einen niedrigeren Rang hat, - // wird dieser ebenfalls befördert usw.! std::vector> soldiersToUpgrade; - // Rang des letzten beförderten Soldaten, MaxRank am Anfang setzen, damit keiner über den maximalen Rang - // befördert wird - uint8_t last_rank = world->GetGGS().GetMaxMilitaryRank(); - for(auto it = troops.rbegin(); it != troops.rend();) + const uint8_t maxRank = world->GetGGS().GetMaxMilitaryRank(); + + if(world->GetGGS().isEnabled(AddonId::SINGLE_SOLDIER_COIN_TRAINING)) { - auto& soldier = *it; - // Es wurde schon einer befördert, dieser Soldat muss nun einen niedrigeren Rang - // als der letzte haben, damit er auch noch befördert werden kann - if(soldier->GetRank() < last_rank) + auto it = std::find_if(troops.begin(), troops.end(), + [maxRank](const auto& soldier) { return soldier->GetRank() < maxRank; }); + if(it != troops.end()) { - // Rang merken - last_rank = soldier->GetRank(); - // Remove from sorted container as changing it breaks sorting - soldiersToUpgrade.push_back(std::move(soldier)); - it = helpers::erase_reverse(troops, it); - } else - ++it; + soldiersToUpgrade.push_back(std::move(*it)); + troops.erase(it); + } + } else + { + // Soldaten befördern + // Von hinten durchgehen + // Wenn der nachfolgende (schwächere) Soldat einen niedrigeren Rang hat, + // wird dieser ebenfalls befördert usw.! + uint8_t last_rank = maxRank; + for(auto it = troops.rbegin(); it != troops.rend();) + { + auto& soldier = *it; + // Es wurde schon einer befördert, dieser Soldat muss nun einen niedrigeren Rang + // als der letzte haben, damit er auch noch befördert werden kann + if(soldier->GetRank() < last_rank) + { + // Rang merken + last_rank = soldier->GetRank(); + // Remove from sorted container as changing it breaks sorting + soldiersToUpgrade.push_back(std::move(soldier)); + it = helpers::erase_reverse(troops, it); + } else + ++it; + } } // Wurde jemand befördert? diff --git a/tests/s25Main/integration/testAttacking.cpp b/tests/s25Main/integration/testAttacking.cpp index 3dcb6320f0..daaa42a776 100644 --- a/tests/s25Main/integration/testAttacking.cpp +++ b/tests/s25Main/integration/testAttacking.cpp @@ -61,6 +61,23 @@ auto calcSum(const T& collection) return std::accumulate(std::begin(collection), std::end(collection), 0u); } +std::array CountTroopsByRank(const nobMilitary& bld) +{ + std::array counts{}; + for(const auto& soldier : bld.GetTroops()) + ++counts[soldier.GetRank()]; + return counts; +} + +void DeliverCoin(nobMilitary& bld, GameWorld& world, const MapPoint hqPos) +{ + auto* hq = world.GetSpecObj(hqPos); + BOOST_TEST_REQUIRE(hq); + + hq->AddToInventory(GoodCounts::make(GoodType::Coins, 1), true); + bld.SearchCoins(); +} + /// Reschedule the walk event of the obj to be executed in numGFs GFs void rescheduleWalkEvent(TestEventManager& em, noMovable& obj, unsigned numGFs) { @@ -565,6 +582,44 @@ BOOST_FIXTURE_TEST_CASE(ArmoredSoldierLosesArmorInFight, AttackFixture<>) BOOST_TEST(milBld1->GetDefender()->GetHitpoints() == HITPOINTS[milBld1->GetDefender()->GetRank()]); } +BOOST_FIXTURE_TEST_CASE(CoinTrainingUpgradesRankChainByDefault, AttackFixture<>) +{ + AddSoldiers(milBld0Pos, 1, Job::Private); + AddSoldiers(milBld0Pos, 1, Job::PrivateFirstClass); + AddSoldiers(milBld0Pos, 1, Job::Sergeant); + + BuildRoadForBlds(milBld0Pos, hqPos[0]); + DeliverCoin(*milBld0, world, hqPos[0]); + + RTTR_EXEC_TILL(5000, CountTroopsByRank(*milBld0)[3] == 1u); + + const auto counts = CountTroopsByRank(*milBld0); + BOOST_TEST_REQUIRE(counts[0] == 0u); + BOOST_TEST_REQUIRE(counts[1] == 1u); + BOOST_TEST_REQUIRE(counts[2] == 1u); + BOOST_TEST_REQUIRE(counts[3] == 1u); +} + +BOOST_FIXTURE_TEST_CASE(SingleSoldierCoinTrainingUpgradesOnlyLowestRankSoldier, AttackFixture<>) +{ + this->ggs.setSelection(AddonId::SINGLE_SOLDIER_COIN_TRAINING, 1); + + AddSoldiers(milBld0Pos, 1, Job::Private); + AddSoldiers(milBld0Pos, 1, Job::PrivateFirstClass); + AddSoldiers(milBld0Pos, 1, Job::Sergeant); + + BuildRoadForBlds(milBld0Pos, hqPos[0]); + DeliverCoin(*milBld0, world, hqPos[0]); + + RTTR_EXEC_TILL(5000, CountTroopsByRank(*milBld0)[1] == 2u); + + const auto counts = CountTroopsByRank(*milBld0); + BOOST_TEST_REQUIRE(counts[0] == 0u); + BOOST_TEST_REQUIRE(counts[1] == 2u); + BOOST_TEST_REQUIRE(counts[2] == 1u); + BOOST_TEST_REQUIRE(counts[3] == 0u); +} + BOOST_FIXTURE_TEST_CASE(TroopLimitKeepsOrderedRestrictedSoldier, AttackFixture<>) { MilitarySettings milSettings = MILITARY_SETTINGS_SCALE;