Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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 libs/s25main/GlobalGameSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ void GlobalGameSettings::registerAllAddons()
AddonMoreAnimals,
AddonNoAlliedPush,
AddonNoCoinsDefault,
AddonSingleSoldierCoinTraining,
AddonNumScoutsExploration,
AddonPeacefulMode,
AddonRefundMaterials,
Expand Down
18 changes: 18 additions & 0 deletions libs/s25main/addons/AddonSingleSoldierCoinTraining.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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 lower-rank soldiers."))
Comment thread
DevOpsOfChaos marked this conversation as resolved.
Outdated
{}
};
1 change: 1 addition & 0 deletions libs/s25main/addons/Addons.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "addons/AddonDefenderBehavior.h"

#include "addons/AddonNoCoinsDefault.h"
#include "addons/AddonSingleSoldierCoinTraining.h"

#include "addons/AddonAdjustMilitaryStrength.h"

Expand Down
2 changes: 1 addition & 1 deletion libs/s25main/addons/const_addons.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x
AUTOFLAGS = 0x00F00000,

WINE = 0x01000000, LEATHER = 0x01000001, NO_ARMOR_DEFAULT = 0x01000002,
ARMOR_CAPTURED_BLD = 0x01000003)
ARMOR_CAPTURED_BLD = 0x01000003, SINGLE_SOLDIER_COIN_TRAINING = 0x01000005)
//-V:AddonId:801

enum class AddonGroup : unsigned
Expand Down
51 changes: 32 additions & 19 deletions libs/s25main/buildings/nobMilitary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "gameData/MilitaryConsts.h"
#include "gameData/SettingTypeConv.h"
#include "s25util/Log.h"
#include <algorithm>
#include <limits>
#include <stdexcept>

Expand Down Expand Up @@ -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<std::unique_ptr<nofPassiveSoldier>> 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?
Expand Down
55 changes: 55 additions & 0 deletions tests/s25Main/integration/testAttacking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ auto calcSum(const T& collection)
return std::accumulate(std::begin(collection), std::end(collection), 0u);
}

std::array<unsigned, NUM_SOLDIER_RANKS> CountTroopsByRank(const nobMilitary& bld)
{
std::array<unsigned, NUM_SOLDIER_RANKS> 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<nobBaseWarehouse>(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)
{
Expand Down Expand Up @@ -543,6 +560,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(ConquerBldCoinAddonEnable, AttackFixture<>)
{
this->ggs.setSelection(AddonId::COINS_CAPTURED_BLD, 1); // addon is active on second run
Expand Down
Loading