diff --git a/VisualStudio/fheroes2/sources.props b/VisualStudio/fheroes2/sources.props
index 711ef43a4..f29ebd0bd 100644
--- a/VisualStudio/fheroes2/sources.props
+++ b/VisualStudio/fheroes2/sources.props
@@ -151,6 +151,7 @@
+
@@ -371,6 +372,7 @@
+
diff --git a/src/fheroes2/editor/editor_daily_event_spec_window.cpp b/src/fheroes2/editor/editor_daily_event_spec_window.cpp
index c54b9300b..916fd3b70 100644
--- a/src/fheroes2/editor/editor_daily_event_spec_window.cpp
+++ b/src/fheroes2/editor/editor_daily_event_spec_window.cpp
@@ -34,6 +34,7 @@
#include "dialog.h"
#include "editor_ui_helper.h"
#include "game_hotkeys.h"
+#include "game_tools.h"
#include "image.h"
#include "localevent.h"
#include "map_format_info.h"
@@ -178,7 +179,7 @@ namespace Editor
fheroes2::ImageRestorer firstDateDescription( display, playerAreaOffsetX, offsetY, playerAreaWidth, 35 );
- text.set( getDateDescription( firstDaySelection.getValue() ), fheroes2::FontType::normalWhite() );
+ text.set( Game::getDateDescription( firstDaySelection.getValue() ), fheroes2::FontType::normalWhite() );
text.draw( playerAreaOffsetX + ( playerAreaWidth - text.width() ) / 2, offsetY, text.width(), display );
offsetY += 35;
@@ -223,7 +224,7 @@ namespace Editor
firstDateDescription.restore();
- text.set( getDateDescription( firstDaySelection.getValue() ), fheroes2::FontType::normalWhite() );
+ text.set( Game::getDateDescription( firstDaySelection.getValue() ), fheroes2::FontType::normalWhite() );
text.draw( playerAreaOffsetX + ( playerAreaWidth - text.width() ) / 2, firstDateDescription.y(), text.width(), display );
isRedrawNeeded = true;
diff --git a/src/fheroes2/editor/editor_map_specs_window.cpp b/src/fheroes2/editor/editor_map_specs_window.cpp
index d1f2c7b7e..3e27cf812 100644
--- a/src/fheroes2/editor/editor_map_specs_window.cpp
+++ b/src/fheroes2/editor/editor_map_specs_window.cpp
@@ -49,6 +49,7 @@
#include "game_language.h"
#include "game_over.h"
#include "game_string.h"
+#include "game_tools.h"
#include "icn.h"
#include "image.h"
#include "interface_list.h"
@@ -1728,7 +1729,7 @@ namespace
const int32_t offsetY = roi.y + fheroes2::ValueSelectionDialogElement::getArea().height + 14;
- text.set( Editor::getDateDescription( _outOfTimeValue.getValue() ), fheroes2::FontType::normalWhite() );
+ text.set( Game::getDateDescription( _outOfTimeValue.getValue() ), fheroes2::FontType::normalWhite() );
text.draw( roi.x, offsetY, roi.width, output );
break;
diff --git a/src/fheroes2/editor/editor_ui_helper.cpp b/src/fheroes2/editor/editor_ui_helper.cpp
index cf8022c94..481fcf69c 100644
--- a/src/fheroes2/editor/editor_ui_helper.cpp
+++ b/src/fheroes2/editor/editor_ui_helper.cpp
@@ -28,8 +28,6 @@
#include "image.h"
#include "resource.h"
#include "screen.h"
-#include "tools.h"
-#include "translations.h"
#include "ui_text.h"
#include "ui_tool.h"
@@ -165,18 +163,4 @@ namespace Editor
text.set( std::to_string( resources.gold ), fontType );
text.draw( thirdColumnOffset + ( maxWidth * 2 + midElementOffsetX - text.width() ) / 2, offsetY[1] + 1, output );
}
-
- std::string getDateDescription( const int32_t day )
- {
- std::string message = _( "Day: %{day} Week: %{week} Month: %{month}" );
- int32_t days = day - 1;
- const int32_t month = days / ( 7 * 4 );
- days -= month * ( 7 * 4 );
-
- StringReplace( message, "%{day}", ( days % 7 ) + 1 );
- StringReplace( message, "%{week}", ( days / 7 ) + 1 );
- StringReplace( message, "%{month}", month + 1 );
-
- return message;
- }
}
diff --git a/src/fheroes2/editor/editor_ui_helper.h b/src/fheroes2/editor/editor_ui_helper.h
index 59df296a3..07b636335 100644
--- a/src/fheroes2/editor/editor_ui_helper.h
+++ b/src/fheroes2/editor/editor_ui_helper.h
@@ -1,6 +1,6 @@
/***************************************************************************
* fheroes2: https://github.com/ihhub/fheroes2 *
- * Copyright (C) 2024 - 2025 *
+ * Copyright (C) 2024 - 2026 *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
@@ -74,6 +74,4 @@ namespace Editor
const bool isEvil, const int32_t maxWidth );
void renderResources( const Funds & resources, const fheroes2::Rect & roi, fheroes2::Image & output, std::array & resourceRoi );
-
- std::string getDateDescription( const int32_t day );
}
diff --git a/src/fheroes2/game/game_auto_playtest.cpp b/src/fheroes2/game/game_auto_playtest.cpp
index 60d4f1e68..17f5b8013 100644
--- a/src/fheroes2/game/game_auto_playtest.cpp
+++ b/src/fheroes2/game/game_auto_playtest.cpp
@@ -35,6 +35,7 @@
#include "game_assets.h"
#include "game_delays.h"
#include "game_hotkeys.h"
+#include "game_tools.h"
#include "icn.h"
#include "image.h"
#include "localevent.h"
@@ -43,6 +44,7 @@
#include "mus.h"
#include "pal.h"
#include "players.h"
+#include "race.h"
#include "screen.h"
#include "settings.h"
#include "tools.h"
@@ -64,6 +66,7 @@
namespace
{
constexpr int32_t sliderWidth{ 150 };
+ constexpr int32_t playerStepX{ 80 };
class TextRestorer final : public fheroes2::MovableText
{
@@ -96,7 +99,6 @@ namespace
}
auto & players = conf.GetPlayers();
- players.Init( conf.getCurrentMapInfo() );
players.SetStartGame();
return world.loadResurrectionMap( mapInfo.filename );
@@ -140,7 +142,6 @@ namespace
}
// Display the results.
- constexpr int32_t playerStepX{ 80 };
const int32_t playthroughCount{ static_cast( playtest.getResults().size() ) };
const int32_t playerCount{ static_cast( playtest.getResults().front().size() ) };
@@ -232,6 +233,16 @@ namespace
fheroes2::showStandardTextMessage( _( "Okay" ), _( "Click to close the dialog." ), Dialog::ZERO );
}
+ for ( size_t i = 0; i < playerRects.size(); ++i ) {
+ if ( le.isMouseRightButtonPressedInArea( playerRects[i] ) ) {
+ std::string message = _( "%{color} player" );
+ StringReplace( message, "%{color}", Color::String( availableColors[i] ) );
+
+ fheroes2::showStandardTextMessage( _( "Opponents" ), std::move( message ), Dialog::ZERO );
+ break;
+ }
+ }
+
for ( size_t i = 0; i < availableColors.size(); ++i ) {
if ( le.isMouseRightButtonPressedInArea( playerRects[i] ) ) {
std::string playerString{ _( "Won %{percent}% of playthroughs." ) };
@@ -359,10 +370,10 @@ namespace fheroes2
{
Display & display = Display::instance();
- StandardWindow window( 550, 345, true, display );
+ StandardWindow window( 550, 450, true, display );
const Rect activeArea( window.activeArea() );
- const Settings & conf = Settings::Get();
+ Settings & conf = Settings::Get();
const bool isEvilInterface = conf.isEvilInterfaceEnabled();
const Sprite & titleBox = Assets::getImage( isEvilInterface ? ICN::METALLIC_BORDERED_TEXTBOX_EVIL : ICN::METALLIC_BORDERED_TEXTBOX_GOOD, 0 );
@@ -390,6 +401,7 @@ namespace fheroes2
text.fitToOneRow( optionTextMaxWidth );
text.draw( positionX + optionTextMaxWidth - text.width() - optionTitleOffsetX, positionY + 1, display );
HorizontalSlider playthroughCountSlider{ sliderWidth, { inputPositionX, positionY }, 1, AutoPlaytest::playthroughLimit, autoPlaytest.getMaxPlaythroughs() };
+ playthroughCountSlider.setPopupDialog( _( "autoPlaytest|Number of playthroughs:" ), _( "Set the number of playthroughs." ) );
TextRestorer playthroughCountValue{ display, { valuePositionX, positionY + 2 } };
playthroughCountValue.render( getValueString( autoPlaytest.getMaxPlaythroughs(), AutoPlaytest::playthroughLimit ) );
@@ -399,10 +411,21 @@ namespace fheroes2
text.fitToOneRow( optionTextMaxWidth );
text.draw( positionX + optionTextMaxWidth - text.width() - optionTitleOffsetX, positionY + 1, display );
HorizontalSlider dayCountSlider{ sliderWidth, { inputPositionX, positionY }, 1, AutoPlaytest::dayLimit, autoPlaytest.getMaxDaysInPlaythrough() };
+ dayCountSlider.setPopupDialog( _( "autoPlaytest|Max days per playthrough:" ), _( "Set the maximum number of days within each playthrough is being run." ) );
+
TextRestorer dayCountValue{ display, { valuePositionX, positionY + 2 } };
dayCountValue.render( getValueString( autoPlaytest.getMaxDaysInPlaythrough(), AutoPlaytest::dayLimit ) );
- positionY += ySpacing;
+ positionY += 30;
+
+ text.set( _( "autoPlaytest|Last day of playthrough:" ), FontType::normalWhite() );
+ text.fitToOneRow( optionTextMaxWidth );
+ text.draw( positionX + optionTextMaxWidth - text.width() - optionTitleOffsetX, positionY + 1, display );
+
+ TextRestorer date{ display, { inputPositionX + 5, positionY } };
+ date.render( Game::getDateDescription( autoPlaytest.getMaxDaysInPlaythrough() ) );
+
+ positionY += 30;
const Rect animationCheckboxArea{ renderCheckbox( inputPositionX + 3, positionY, autoPlaytest.isAnimationEnabled(), display, isEvilInterface, true ) };
@@ -419,6 +442,7 @@ namespace fheroes2
auto animationTextAreaRestorer = std::make_unique( display, animationTextOffset.x, animationTextOffset.y, text.width(), text.height() );
text.draw( animationTextOffset.x, animationTextOffset.y, display );
HorizontalSlider speedCountSlider{ sliderWidth, { inputPositionX, positionY }, 1, AutoPlaytest::animationLimit, autoPlaytest.getAnimationSpeed() };
+ speedCountSlider.setPopupDialog( _( "autoPlaytest|Animation speed:" ), _( "Set animation speed during playthrough." ) );
TextRestorer speedCountValue{ display, { valuePositionX, positionY + 2 } };
speedCountValue.render( getValueString( autoPlaytest.getAnimationSpeed(), AutoPlaytest::animationLimit ) );
if ( !autoPlaytest.isAnimationEnabled() ) {
@@ -434,7 +458,41 @@ namespace fheroes2
auto soundsTextAreaRestorer = std::make_unique( display, soundsCheckboxArea.x, soundsCheckboxArea.y, text.width(), text.height() );
text.draw( soundsCheckboxArea.x + soundsCheckboxArea.width + 5, soundsCheckboxArea.y + 2, display );
- positionY += ySpacing;
+ positionY += 30;
+
+ // Render players as all computers but with color names.
+ auto & players = conf.GetPlayers();
+ players.Init( conf.getCurrentMapInfo() );
+
+ const int32_t playerCount = static_cast( players.size() );
+ const int32_t playerOffsetX{ activeArea.x + ( activeArea.width - ( ( playerCount - 1 ) * playerStepX + 62 ) ) / 2 };
+
+ std::vector playerColorRect;
+ std::vector playerRaceRect;
+ playerColorRect.resize( playerCount );
+ playerRaceRect.resize( playerCount );
+
+ const Sprite & playerIconShadow = Assets::getImage( ICN::NGEXTRA, 61 );
+
+ for ( int32_t playerId = 0; playerId < playerCount; ++playerId ) {
+ const auto * player = players[playerId];
+ const uint32_t icnIndex = Color::GetIndex( player->GetColor() ) + 3;
+ const Sprite & playerIcon = Assets::getImage( ICN::NGEXTRA, icnIndex );
+
+ playerColorRect[playerId] = Rect( playerOffsetX + playerId * playerStepX, positionY, playerIcon.width(), playerIcon.height() );
+
+ Copy( playerIcon, 0, 0, display, playerColorRect[playerId].x, playerColorRect[playerId].y, playerColorRect[playerId].width,
+ playerColorRect[playerId].height );
+ Blit( playerIconShadow, display, playerColorRect[playerId].x - 5, playerColorRect[playerId].y + 3 );
+
+ const Sprite & classIcon = Assets::getImage( ICN::NGEXTRA, Race::getRaceIcnIndex( player->GetRace(), true ) );
+
+ playerRaceRect[playerId] = Rect( playerColorRect[playerId].x, playerColorRect[playerId].y + 60, classIcon.width(), classIcon.height() );
+ Copy( classIcon, 0, 0, display, playerRaceRect[playerId].x, playerRaceRect[playerId].y, playerRaceRect[playerId].width, playerRaceRect[playerId].height );
+ Blit( playerIconShadow, display, playerRaceRect[playerId].x - 5, playerRaceRect[playerId].y + 3 );
+ }
+
+ positionY += 120;
text.set( _( "Left-clicking at any point will interrupt the playtest." ), FontType::normalYellow() );
text.draw( positionX, positionY, activeArea.width, display );
@@ -470,6 +528,7 @@ namespace fheroes2
else if ( dayCountSlider.processEvents( eventHandler ) ) {
autoPlaytest.setMaxDaysInPlaythrough( dayCountSlider.getCurrentValue() );
dayCountValue.render( getValueString( dayCountSlider.getCurrentValue(), AutoPlaytest::dayLimit ) );
+ date.render( Game::getDateDescription( autoPlaytest.getMaxDaysInPlaythrough() ) );
display.render( window.activeArea() );
}
else if ( autoPlaytest.isAnimationEnabled() && speedCountSlider.processEvents( eventHandler ) ) {
@@ -512,6 +571,44 @@ namespace fheroes2
autoPlaytest.isAnimationEnabled() );
display.render( soundsCheckboxArea );
}
+ else {
+ for ( size_t i = 0; i < playerRaceRect.size(); ++i ) {
+ auto * player = players[i];
+
+ if ( eventHandler.isMouseRightButtonPressedInArea( playerRaceRect[i] ) ) {
+ showStandardTextMessage(
+ _( "Class" ),
+ _( "This lets you change the class of a player. Classes are not always changeable. Depending on the scenario, a player may receive additional towns and/or heroes not of their primary alignment." ),
+ Dialog::ZERO );
+ break;
+ }
+
+ if ( !conf.getCurrentMapInfo().AllowChangeRace( player->GetColor() ) ) {
+ continue;
+ }
+
+ if ( eventHandler.MouseClickLeft( playerRaceRect[i] ) ) {
+ player->SetRace( Race::getNextRace( player->GetRace() ) );
+
+ const Sprite & classIcon = Assets::getImage( ICN::NGEXTRA, Race::getRaceIcnIndex( player->GetRace(), true ) );
+ Copy( classIcon, 0, 0, display, playerRaceRect[i].x, playerRaceRect[i].y, playerRaceRect[i].width, playerRaceRect[i].height );
+
+ display.render( playerRaceRect[i] );
+ break;
+ }
+ }
+
+ for ( size_t i = 0; i < playerColorRect.size(); ++i ) {
+ if ( eventHandler.isMouseRightButtonPressedInArea( playerColorRect[i] ) ) {
+ const auto * player = players[i];
+ std::string message = _( "%{color} player" );
+ StringReplace( message, "%{color}", Color::String( player->GetColor() ) );
+
+ showStandardTextMessage( _( "Opponents" ), std::move( message ), Dialog::ZERO );
+ break;
+ }
+ }
+ }
if ( eventHandler.isMouseRightButtonPressedInArea( buttonOk.area() ) ) {
showStandardTextMessage( _( "Okay" ), _( "Click to run an automated map playtest." ), Dialog::ZERO );
@@ -519,6 +616,12 @@ namespace fheroes2
else if ( eventHandler.isMouseRightButtonPressedInArea( buttonCancel.area() ) ) {
showStandardTextMessage( _( "Cancel" ), _( "Return to the previous menu." ), Dialog::ZERO );
}
+ else if ( eventHandler.isMouseRightButtonPressedInArea( animationCheckboxArea ) ) {
+ showStandardTextMessage( _( "autoPlaytest|Animation" ), _( "Toggle animation during playthroughs." ), Dialog::ZERO );
+ }
+ else if ( eventHandler.isMouseRightButtonPressedInArea( soundsCheckboxArea ) ) {
+ showStandardTextMessage( _( "autoPlaytest|Sound Effects" ), _( "Toggle sound effects during playthroughs." ), Dialog::ZERO );
+ }
}
return false;
diff --git a/src/fheroes2/game/game_auto_playtest.h b/src/fheroes2/game/game_auto_playtest.h
index ff825e5c8..654d25d93 100644
--- a/src/fheroes2/game/game_auto_playtest.h
+++ b/src/fheroes2/game/game_auto_playtest.h
@@ -49,7 +49,9 @@ namespace fheroes2
};
static constexpr int32_t playthroughLimit{ 100 };
- static constexpr int32_t dayLimit{ 1000 };
+
+ // Let's be reasonable. Having a playthrough over 10 years is an overkill.
+ static constexpr int32_t dayLimit{ 3360 };
static constexpr int32_t animationLimit{ 10 };
static AutoPlaytest & instance();
@@ -194,7 +196,7 @@ namespace fheroes2
std::vector> _playthroughResults;
int32_t _maxPlaythroughs{ 1 };
- int32_t _maxDaysInPlaythrough{ 365 };
+ int32_t _maxDaysInPlaythrough{ 336 };
int32_t _animationSpeed{ animationLimit };
bool _isAnimationEnabled{ true };
bool _playEnvironmentSounds{ true };
diff --git a/src/fheroes2/game/game_tools.cpp b/src/fheroes2/game/game_tools.cpp
new file mode 100644
index 000000000..636b3f3ad
--- /dev/null
+++ b/src/fheroes2/game/game_tools.cpp
@@ -0,0 +1,41 @@
+/***************************************************************************
+ * fheroes2: https://github.com/ihhub/fheroes2 *
+ * Copyright (C) 2026 *
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the *
+ * Free Software Foundation, Inc., *
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
+ ***************************************************************************/
+
+#include "game_tools.h"
+
+#include "tools.h"
+#include "translations.h"
+
+namespace Game
+{
+ std::string getDateDescription( const int32_t day )
+ {
+ std::string message = _( "Day: %{day} Week: %{week} Month: %{month}" );
+ int32_t days = day - 1;
+ const int32_t month = days / ( 7 * 4 );
+ days -= month * ( 7 * 4 );
+
+ StringReplace( message, "%{day}", ( days % 7 ) + 1 );
+ StringReplace( message, "%{week}", ( days / 7 ) + 1 );
+ StringReplace( message, "%{month}", month + 1 );
+
+ return message;
+ }
+}
diff --git a/src/fheroes2/game/game_tools.h b/src/fheroes2/game/game_tools.h
new file mode 100644
index 000000000..f5b317c21
--- /dev/null
+++ b/src/fheroes2/game/game_tools.h
@@ -0,0 +1,29 @@
+/***************************************************************************
+ * fheroes2: https://github.com/ihhub/fheroes2 *
+ * Copyright (C) 2026 *
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the *
+ * Free Software Foundation, Inc., *
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
+ ***************************************************************************/
+
+#pragma once
+
+#include
+#include
+
+namespace Game
+{
+ std::string getDateDescription( const int32_t day );
+}
diff --git a/src/fheroes2/gui/ui_slider.cpp b/src/fheroes2/gui/ui_slider.cpp
index a9788067e..5ac57fa2b 100644
--- a/src/fheroes2/gui/ui_slider.cpp
+++ b/src/fheroes2/gui/ui_slider.cpp
@@ -25,6 +25,7 @@
#include
#include
+#include "dialog.h"
#include "game_assets.h"
#include "icn.h"
#include "image.h"
@@ -32,6 +33,7 @@
#include "pal.h"
#include "screen.h"
#include "settings.h"
+#include "ui_dialog.h"
namespace fheroes2
{
@@ -118,6 +120,10 @@ namespace fheroes2
return true;
}
+ if ( !_popupTitle.empty() && !_popupDescription.empty() && le.isMouseRightButtonPressedInArea( _scrollbar.getArea() ) ) {
+ fheroes2::showStandardTextMessage( _popupTitle, _popupDescription, Dialog::ZERO );
+ }
+
return false;
}
diff --git a/src/fheroes2/gui/ui_slider.h b/src/fheroes2/gui/ui_slider.h
index 3e906402c..6b47d950f 100644
--- a/src/fheroes2/gui/ui_slider.h
+++ b/src/fheroes2/gui/ui_slider.h
@@ -21,6 +21,8 @@
#pragma once
#include
+#include
+#include
#include "image.h"
#include "math_base.h"
@@ -54,6 +56,12 @@ namespace fheroes2
void enable();
+ void setPopupDialog( std::string title, std::string description )
+ {
+ _popupTitle = std::move( title );
+ _popupDescription = std::move( description );
+ }
+
private:
Scrollbar _scrollbar;
Button _buttonLeft;
@@ -63,5 +71,8 @@ namespace fheroes2
TimedEventValidator _timedButtonLeft;
TimedEventValidator _timedButtonRight;
+
+ std::string _popupTitle;
+ std::string _popupDescription;
};
}