Rework Auto-playtest dialog (#11039)

This commit is contained in:
Ihar Hubchyk
2026-10-10 11:21:09 +08:00
committed by GitHub
parent b1b25529ea
commit c0eac6d261
11 changed files with 208 additions and 30 deletions
+2
View File
@@ -151,6 +151,7 @@
<ClCompile Include="src\fheroes2\game\game_startgame.cpp" />
<ClCompile Include="src\fheroes2\game\game_static.cpp" />
<ClCompile Include="src\fheroes2\game\game_string.cpp" />
<ClCompile Include="src\fheroes2\game\game_tools.cpp" />
<ClCompile Include="src\fheroes2\game\game_video.cpp" />
<ClCompile Include="src\fheroes2\game\highscores.cpp" />
<ClCompile Include="src\fheroes2\gui\cursor.cpp" />
@@ -371,6 +372,7 @@
<ClInclude Include="src\fheroes2\game\game_over.h" />
<ClInclude Include="src\fheroes2\game\game_static.h" />
<ClInclude Include="src\fheroes2\game\game_string.h" />
<ClInclude Include="src\fheroes2\game\game_tools.h" />
<ClInclude Include="src\fheroes2\game\game_video.h" />
<ClInclude Include="src\fheroes2\game\game_video_type.h" />
<ClInclude Include="src\fheroes2\game\highscores.h" />
@@ -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;
@@ -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;
-16
View File
@@ -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;
}
}
+1 -3
View File
@@ -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<fheroes2::Rect, 7> & resourceRoi );
std::string getDateDescription( const int32_t day );
}
+109 -6
View File
@@ -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<int32_t>( playtest.getResults().size() ) };
const int32_t playerCount{ static_cast<int32_t>( 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<ImageRestorer>( 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<ImageRestorer>( 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<int32_t>( players.size() );
const int32_t playerOffsetX{ activeArea.x + ( activeArea.width - ( ( playerCount - 1 ) * playerStepX + 62 ) ) / 2 };
std::vector<Rect> playerColorRect;
std::vector<Rect> 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;
+4 -2
View File
@@ -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<std::vector<PlayerInfo>> _playthroughResults;
int32_t _maxPlaythroughs{ 1 };
int32_t _maxDaysInPlaythrough{ 365 };
int32_t _maxDaysInPlaythrough{ 336 };
int32_t _animationSpeed{ animationLimit };
bool _isAnimationEnabled{ true };
bool _playEnvironmentSounds{ true };
+41
View File
@@ -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;
}
}
+29
View File
@@ -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 <cstdint>
#include <string>
namespace Game
{
std::string getDateDescription( const int32_t day );
}
+6
View File
@@ -25,6 +25,7 @@
#include <functional>
#include <utility>
#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;
}
+11
View File
@@ -21,6 +21,8 @@
#pragma once
#include <cstdint>
#include <string>
#include <utility>
#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;
};
}