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; }; }