diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index f5047f9893..c8a686abb4 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -91,6 +91,7 @@ void GlobalGameSettings::registerAllAddons() AddonMoreAnimals, AddonNoAlliedPush, AddonNoCoinsDefault, + AddonStrandedSoldierReturnSearch, AddonNumScoutsExploration, AddonPeacefulMode, AddonRefundMaterials, diff --git a/libs/s25main/addons/AddonStrandedSoldierReturnSearch.h b/libs/s25main/addons/AddonStrandedSoldierReturnSearch.h new file mode 100644 index 0000000000..44563a2d5c --- /dev/null +++ b/libs/s25main/addons/AddonStrandedSoldierReturnSearch.h @@ -0,0 +1,24 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "AddonList.h" +#include "mygettext/mygettext.h" + +class AddonStrandedSoldierReturnSearch : public AddonList +{ +public: + AddonStrandedSoldierReturnSearch() + : AddonList( + AddonId::STRANDED_SOLDIER_RETURN_SEARCH, AddonGroup::Military, _("Stranded soldier return search"), + _("Controls the search radius used by stranded soldiers when looking for a way back to a warehouse."), + { + _("Default search range (1x)"), + _("Reduced search range (0.5x)"), + _("Extended search range (2x)"), + _("Very large search range (4x)"), + }) + {} +}; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 96b3203ff7..8d11335335 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/AddonStrandedSoldierReturnSearch.h" #include "addons/AddonAdjustMilitaryStrength.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index f7ddf54f08..6d9d6a0fc6 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, + STRANDED_SOLDIER_RETURN_SEARCH = 0x01100001, + FORESTER_REACH_RADIUS = 0x01200000, WOODCUTTER_REACH_RADIUS = 0x01200001, STONEMASON_REACH_RADIUS = 0x01200002) //-V:AddonId:801 diff --git a/libs/s25main/figures/noFigure.cpp b/libs/s25main/figures/noFigure.cpp index f4df49958e..91e67b9bc9 100644 --- a/libs/s25main/figures/noFigure.cpp +++ b/libs/s25main/figures/noFigure.cpp @@ -46,6 +46,7 @@ const unsigned short WANDER_RADIUS = 10; /// Dasselbe nochmal für Soldaten const unsigned short WANDER_TRYINGS_SOLDIERS = 6; const unsigned short WANDER_RADIUS_SOLDIERS = 15; +const unsigned short STRANDED_SOLDIER_RETURN_SEARCH_RADIUS_REDUCED = WANDER_RADIUS_SOLDIERS / 2; noFigure::noFigure(const Job job, const MapPoint pos, const unsigned char player, noRoadNode* const goal) : noMovable(NodalObjectType::Figure, pos), fs(FigureState::GotToGoal), job_(job), player(player), cur_rs(nullptr), @@ -464,7 +465,7 @@ void noFigure::StartWandering(const unsigned burned_wh_id) // 3x rumirren und eine Flagge suchen, wenn dann keine gefunden wurde, stirbt die Figur wander_way = WANDER_WAY_MIN + RANDOM_RAND(WANDER_WAY_MAX - WANDER_WAY_MIN); // Soldaten sind härter im Nehmen - wander_tryings = IsSoldier() ? WANDER_TRYINGS_SOLDIERS : WANDER_TRYINGS; + wander_tryings = IsSoldier() ? GetStrandedSoldierReturnSearchTryings(world->GetGGS()) : WANDER_TRYINGS; // Wenn wir stehen, zusätzlich noch loslaufen! if(waiting_for_free_node) @@ -492,8 +493,45 @@ struct IsValidFlag IsValidFlag(const unsigned playerId) : playerId_(playerId) {} bool operator()(const noFlag* const flag) const { return flag && flag->GetPlayer() == playerId_; } }; + +enum class StrandedSoldierReturnSearchSelection +{ + ReducedRadius = 1, + WideSecondStage = 2, + WideThirdStage = 3 +}; + } // namespace +unsigned short GetStrandedSoldierReturnSearchTryings(const GlobalGameSettings& ggs) +{ + switch(static_cast(ggs.getSelection(AddonId::STRANDED_SOLDIER_RETURN_SEARCH))) + { + case StrandedSoldierReturnSearchSelection::WideSecondStage: return 2 * WANDER_TRYINGS_SOLDIERS; + case StrandedSoldierReturnSearchSelection::WideThirdStage: return 3 * WANDER_TRYINGS_SOLDIERS; + default: return WANDER_TRYINGS_SOLDIERS; + } +} + +unsigned short GetStrandedSoldierReturnSearchRadius(const GlobalGameSettings& ggs, const unsigned short wanderTriesLeft) +{ + switch(static_cast(ggs.getSelection(AddonId::STRANDED_SOLDIER_RETURN_SEARCH))) + { + case StrandedSoldierReturnSearchSelection::ReducedRadius: return STRANDED_SOLDIER_RETURN_SEARCH_RADIUS_REDUCED; + case StrandedSoldierReturnSearchSelection::WideSecondStage: + // Start with the normal soldier radius, then widen the final stage after the first six failed searches. + return wanderTriesLeft > WANDER_TRYINGS_SOLDIERS ? WANDER_RADIUS_SOLDIERS : 2 * WANDER_RADIUS_SOLDIERS; + case StrandedSoldierReturnSearchSelection::WideThirdStage: + // Escalate after each stage: normal radius, then 2x, then 4x for the final stage. + if(wanderTriesLeft > 2 * WANDER_TRYINGS_SOLDIERS) + return WANDER_RADIUS_SOLDIERS; + if(wanderTriesLeft > WANDER_TRYINGS_SOLDIERS) + return 2 * WANDER_RADIUS_SOLDIERS; + return 4 * WANDER_RADIUS_SOLDIERS; + default: return WANDER_RADIUS_SOLDIERS; + } +} + void noFigure::Wander() { // Sind wir noch auf der Suche nach einer Flagge? @@ -508,7 +546,8 @@ void noFigure::Wander() if(!wander_way) { // Soldaten sind härter im Nehmen - const unsigned short wander_radius = IsSoldier() ? WANDER_RADIUS_SOLDIERS : WANDER_RADIUS; + const unsigned short wander_radius = + IsSoldier() ? GetStrandedSoldierReturnSearchRadius(world->GetGGS(), wander_tryings) : WANDER_RADIUS; // Flaggen sammeln und dann zufällig eine auswählen const std::vector flags = diff --git a/libs/s25main/figures/noFigure.h b/libs/s25main/figures/noFigure.h index b52a20c630..75a8cb4255 100644 --- a/libs/s25main/figures/noFigure.h +++ b/libs/s25main/figures/noFigure.h @@ -30,6 +30,13 @@ constexpr auto maxEnumValue(FigureState) } class SerializedGameData; +class GlobalGameSettings; + +/// Number of flag-search attempts for stranded soldiers looking for a return path to an own warehouse. +unsigned short GetStrandedSoldierReturnSearchTryings(const GlobalGameSettings& ggs); +/// Radius used only for stranded soldiers looking for a return path to an own warehouse. +/// Normal worker wandering keeps using WANDER_RADIUS and is not affected by this addon. +unsigned short GetStrandedSoldierReturnSearchRadius(const GlobalGameSettings& ggs, unsigned short wanderTriesLeft); // Stellt einen Menschen dar class noFigure : public noMovable diff --git a/tests/s25Main/integration/testFigures.cpp b/tests/s25Main/integration/testFigures.cpp index dcd9514a3c..f1c7f6789a 100644 --- a/tests/s25Main/integration/testFigures.cpp +++ b/tests/s25Main/integration/testFigures.cpp @@ -3,8 +3,10 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "GamePlayer.h" +#include "GlobalGameSettings.h" #include "PointOutput.h" #include "RttrForeachPt.h" +#include "addons/const_addons.h" #include "buildings/nobBaseWarehouse.h" #include "factories/BuildingFactory.h" #include "figures/noFigure.h" @@ -21,6 +23,53 @@ BOOST_AUTO_TEST_SUITE(FigureTests) +BOOST_AUTO_TEST_CASE(StrandedSoldierReturnSearchDefaultKeepsNormalTryingsAndRadius) +{ + GlobalGameSettings ggs; + ggs.setSelection(AddonId::STRANDED_SOLDIER_RETURN_SEARCH, 0); + + BOOST_TEST(GetStrandedSoldierReturnSearchTryings(ggs) == 6u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 6) == 15u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 1) == 15u); +} + +BOOST_AUTO_TEST_CASE(StrandedSoldierReturnSearchReducedKeepsNormalTryingsAndFixedHalfRadius) +{ + GlobalGameSettings ggs; + ggs.setSelection(AddonId::STRANDED_SOLDIER_RETURN_SEARCH, 1); + + BOOST_TEST(GetStrandedSoldierReturnSearchTryings(ggs) == 6u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 12) == 7u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 6) == 7u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 1) == 7u); +} + +BOOST_AUTO_TEST_CASE(StrandedSoldierReturnSearchExtendedEscalatesAfterNormalTryings) +{ + GlobalGameSettings ggs; + ggs.setSelection(AddonId::STRANDED_SOLDIER_RETURN_SEARCH, 2); + + BOOST_TEST(GetStrandedSoldierReturnSearchTryings(ggs) == 12u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 12) == 15u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 7) == 15u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 6) == 30u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 1) == 30u); +} + +BOOST_AUTO_TEST_CASE(StrandedSoldierReturnSearchVeryLargeEscalatesThroughThreeStages) +{ + GlobalGameSettings ggs; + ggs.setSelection(AddonId::STRANDED_SOLDIER_RETURN_SEARCH, 3); + + BOOST_TEST(GetStrandedSoldierReturnSearchTryings(ggs) == 18u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 18) == 15u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 13) == 15u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 12) == 30u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 7) == 30u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 6) == 60u); + BOOST_TEST(GetStrandedSoldierReturnSearchRadius(ggs, 1) == 60u); +} + BOOST_FIXTURE_TEST_CASE(DestroyWHWithFigure, WorldWithGCExecution2P) { world.GetPlayer(curPlayer).GetFirstWH()->AddToInventory(PeopleCounts::make(Job::Helper, 10), true);