From 2d4b7df3a9de1bc4d59e7b5cecc17d496c912d04 Mon Sep 17 00:00:00 2001 From: Zenseii Date: Sat, 18 Jan 2025 05:42:34 +0100 Subject: [PATCH] Add dialog to choose between Auto Combat and Quick Combat (#9180) --- src/fheroes2/agg/agg_image.cpp | 34 +++- src/fheroes2/agg/icn.h | 5 + src/fheroes2/ai/ai_battle.cpp | 14 +- src/fheroes2/ai/ai_battle.h | 4 +- src/fheroes2/battle/battle_action.cpp | 24 +-- src/fheroes2/battle/battle_arena.cpp | 27 ++-- src/fheroes2/battle/battle_arena.h | 14 +- src/fheroes2/battle/battle_command.cpp | 6 +- src/fheroes2/battle/battle_command.h | 10 +- src/fheroes2/battle/battle_interface.cpp | 153 ++++++++++++++---- src/fheroes2/battle/battle_interface.h | 9 +- src/fheroes2/dialog/dialog_system_options.cpp | 4 +- src/fheroes2/game/game_hotkeys.cpp | 26 ++- src/fheroes2/game/game_hotkeys.h | 4 +- src/fheroes2/gui/ui_button.cpp | 31 +++- 15 files changed, 265 insertions(+), 100 deletions(-) diff --git a/src/fheroes2/agg/agg_image.cpp b/src/fheroes2/agg/agg_image.cpp index ed5780ffe49..ef06fb4cefb 100644 --- a/src/fheroes2/agg/agg_image.cpp +++ b/src/fheroes2/agg/agg_image.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2021 - 2024 * + * Copyright (C) 2021 - 2025 * * * * 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 * @@ -188,7 +188,11 @@ namespace ICN::BUTTON_EVENTS_GOOD, ICN::BUTTON_EVENTS_EVIL, ICN::BUTTON_LANGUAGE_GOOD, - ICN::BUTTON_LANGUAGE_EVIL }; + ICN::BUTTON_LANGUAGE_EVIL, + ICN::BUTTON_AUTO_COMBAT_GOOD, + ICN::BUTTON_AUTO_COMBAT_EVIL, + ICN::BUTTON_QUICK_COMBAT_GOOD, + ICN::BUTTON_QUICK_COMBAT_EVIL }; #ifndef NDEBUG bool isLanguageDependentIcnId( const int id ) @@ -2127,6 +2131,28 @@ namespace break; } + case ICN::BUTTON_AUTO_COMBAT_GOOD: + case ICN::BUTTON_AUTO_COMBAT_EVIL: { + _icnVsSprite[id].resize( 2 ); + + const bool isEvilInterface = ( id == ICN::BUTTON_AUTO_COMBAT_EVIL ); + + getTextAdaptedButton( _icnVsSprite[id][0], _icnVsSprite[id][1], gettext_noop( "AUTO\nCOMBAT" ), + isEvilInterface ? ICN::EMPTY_EVIL_BUTTON : ICN::EMPTY_GOOD_BUTTON, isEvilInterface ? ICN::STONEBAK_EVIL : ICN::STONEBAK ); + + break; + } + case ICN::BUTTON_QUICK_COMBAT_GOOD: + case ICN::BUTTON_QUICK_COMBAT_EVIL: { + _icnVsSprite[id].resize( 2 ); + + const bool isEvilInterface = ( id == ICN::BUTTON_QUICK_COMBAT_EVIL ); + + getTextAdaptedButton( _icnVsSprite[id][0], _icnVsSprite[id][1], gettext_noop( "QUICK\nCOMBAT" ), + isEvilInterface ? ICN::EMPTY_EVIL_BUTTON : ICN::EMPTY_GOOD_BUTTON, isEvilInterface ? ICN::STONEBAK_EVIL : ICN::STONEBAK ); + + break; + } default: // You're calling this function for non-specified ICN id. Check your logic! // Did you add a new image for one language without generating a default @@ -2839,6 +2865,10 @@ namespace case ICN::BUTTON_EVENTS_EVIL: case ICN::BUTTON_LANGUAGE_GOOD: case ICN::BUTTON_LANGUAGE_EVIL: + case ICN::BUTTON_AUTO_COMBAT_GOOD: + case ICN::BUTTON_AUTO_COMBAT_EVIL: + case ICN::BUTTON_QUICK_COMBAT_GOOD: + case ICN::BUTTON_QUICK_COMBAT_EVIL: generateLanguageSpecificImages( id ); return true; case ICN::PHOENIX: diff --git a/src/fheroes2/agg/icn.h b/src/fheroes2/agg/icn.h index c4b269c4b9c..9ae928f0e44 100644 --- a/src/fheroes2/agg/icn.h +++ b/src/fheroes2/agg/icn.h @@ -1126,6 +1126,11 @@ namespace ICN BUTTON_LANGUAGE_GOOD, BUTTON_LANGUAGE_EVIL, + BUTTON_AUTO_COMBAT_GOOD, + BUTTON_AUTO_COMBAT_EVIL, + BUTTON_QUICK_COMBAT_GOOD, + BUTTON_QUICK_COMBAT_EVIL, + SCENIBKG_EVIL, // IMPORTANT! Put any new entry just above this one. diff --git a/src/fheroes2/ai/ai_battle.cpp b/src/fheroes2/ai/ai_battle.cpp index 1dcf6e87f2a..567d9e65f64 100644 --- a/src/fheroes2/ai/ai_battle.cpp +++ b/src/fheroes2/ai/ai_battle.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2024 * + * Copyright (C) 2024 - 2025 * * * * 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 * @@ -659,13 +659,13 @@ bool AI::BattlePlanner::isLimitOfTurnsExceeded( const Battle::Arena & arena, Bat // We have gone beyond the limit on the number of turns without deaths and have to stop if ( _numberOfRemainingTurnsWithoutDeaths == 0 ) { - // If this is an auto battle (and not the instant battle, because the battle UI is present), then turn it off until the end of the battle - if ( arena.AutoBattleInProgress() && Battle::Arena::GetInterface() != nullptr ) { - assert( arena.CanToggleAutoBattle() ); + // If this is an auto combat (and not a quick combat, because the battle UI is present), then turn it off until the end of the battle + if ( arena.AutoCombatInProgress() && Battle::Arena::GetInterface() != nullptr ) { + assert( arena.CanToggleAutoCombat() ); - actions.emplace_back( Battle::Command::AUTO_SWITCH, currentColor ); + actions.emplace_back( Battle::Command::TOGGLE_AUTO_COMBAT, currentColor ); - DEBUG_LOG( DBG_BATTLE, DBG_INFO, Color::String( currentColor ) << " has used up the limit of turns without deaths, auto battle is turned off" ) + DEBUG_LOG( DBG_BATTLE, DBG_INFO, Color::String( currentColor ) << " has used up the limit of turns without deaths, auto combat is turned off" ) } // Otherwise the attacker's hero should retreat else { @@ -711,7 +711,7 @@ Battle::Actions AI::BattlePlanner::planUnitTurn( Battle::Arena & arena, const Ba return Outcome::ContinueBattle; } - // Human-controlled heroes should not retreat or surrender during auto/instant battles + // Human-controlled heroes should not retreat or surrender during auto/quick combat if ( actualHero->isControlHuman() ) { return Outcome::ContinueBattle; } diff --git a/src/fheroes2/ai/ai_battle.h b/src/fheroes2/ai/ai_battle.h index 56cce8f24f1..0a673fc1b9b 100644 --- a/src/fheroes2/ai/ai_battle.h +++ b/src/fheroes2/ai/ai_battle.h @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2024 * + * Copyright (C) 2024 - 2025 * * * * 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 * @@ -112,7 +112,7 @@ namespace AI int32_t spellDurationMultiplier( const Battle::Unit & target ) const; // When this limit of turns without deaths is exceeded for an attacking AI-controlled hero, - // the auto battle should be interrupted (one way or another) + // the auto combat should be interrupted (one way or another) static const uint32_t MAX_TURNS_WITHOUT_DEATHS = 50; // Member variables related to the logic of checking the limit of the number of turns diff --git a/src/fheroes2/battle/battle_action.cpp b/src/fheroes2/battle/battle_action.cpp index 06f99045599..78f81d6382c 100644 --- a/src/fheroes2/battle/battle_action.cpp +++ b/src/fheroes2/battle/battle_action.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2019 - 2024 * + * Copyright (C) 2019 - 2025 * * * * Free Heroes2 Engine: http://sourceforge.net/projects/fheroes2 * * Copyright (C) 2010 by Andrey Afletdinov * @@ -424,11 +424,11 @@ void Battle::Arena::ApplyAction( Command & cmd ) ApplyActionSurrender( cmd ); break; - case CommandType::AUTO_SWITCH: - ApplyActionAutoSwitch( cmd ); + case CommandType::TOGGLE_AUTO_COMBAT: + ApplyActionToggleAutoCombat( cmd ); break; - case CommandType::AUTO_FINISH: - ApplyActionAutoFinish( cmd ); + case CommandType::QUICK_COMBAT: + ApplyActionQuickCombat( cmd ); break; default: @@ -1325,7 +1325,7 @@ void Battle::Arena::ApplyActionCatapult( Command & cmd ) } } -void Battle::Arena::ApplyActionAutoSwitch( Command & cmd ) +void Battle::Arena::ApplyActionToggleAutoCombat( Command & cmd ) { const auto checkParameters = []( const int color ) { const Arena * arena = GetArena(); @@ -1355,22 +1355,22 @@ void Battle::Arena::ApplyActionAutoSwitch( Command & cmd ) return; } - _autoBattleColors ^= color; + _autoCombatColors ^= color; - DEBUG_LOG( DBG_BATTLE, DBG_TRACE, "color: " << Color::String( color ) << ", status: " << ( ( _autoBattleColors & color ) ? "on" : "off" ) ) + DEBUG_LOG( DBG_BATTLE, DBG_TRACE, "color: " << Color::String( color ) << ", status: " << ( ( _autoCombatColors & color ) ? "on" : "off" ) ) if ( _interface ) { const Player * player = Players::Get( color ); assert( player ); - std::string msg = ( _autoBattleColors & color ) ? _( "%{name} has turned on the auto battle" ) : _( "%{name} has turned off the auto battle" ); + std::string msg = ( _autoCombatColors & color ) ? _( "%{name} has turned on the auto combat" ) : _( "%{name} has turned off the auto combat" ); StringReplace( msg, "%{name}", player->GetName() ); _interface->setStatus( msg, true ); } } -void Battle::Arena::ApplyActionAutoFinish( const Command & /* cmd */ ) +void Battle::Arena::ApplyActionQuickCombat( const Command & /* cmd */ ) { const int army1Control = GetForce1().GetControl(); const int army2Control = GetForce2().GetControl(); @@ -1391,10 +1391,10 @@ void Battle::Arena::ApplyActionAutoFinish( const Command & /* cmd */ ) const int army2Color = GetArmy2Color(); if ( army1Control & CONTROL_HUMAN ) { - _autoBattleColors |= army1Color; + _autoCombatColors |= army1Color; } if ( army2Control & CONTROL_HUMAN ) { - _autoBattleColors |= army2Color; + _autoCombatColors |= army2Color; } _interface.reset(); diff --git a/src/fheroes2/battle/battle_arena.cpp b/src/fheroes2/battle/battle_arena.cpp index c5001abbc72..fa55d23946e 100644 --- a/src/fheroes2/battle/battle_arena.cpp +++ b/src/fheroes2/battle/battle_arena.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2019 - 2024 * + * Copyright (C) 2019 - 2025 * * * * Free Heroes2 Engine: http://sourceforge.net/projects/fheroes2 * * Copyright (C) 2010 by Andrey Afletdinov * @@ -362,7 +362,6 @@ Battle::Arena::Arena( Army & army1, Army & army2, const int32_t tileIndex, const castle = nullptr; } - // init interface if ( isShowInterface ) { _interface = std::make_unique( *this, tileIndex ); board.SetArea( _interface->GetArea() ); @@ -372,12 +371,12 @@ Battle::Arena::Arena( Army & army1, Army & army2, const int32_t tileIndex, const _interface->SetOrderOfUnits( _orderOfUnits ); } else { - // no interface - force auto battle mode for human player + // There is no interface - force the auto combat mode for the human player if ( army1.isControlHuman() ) { - _autoBattleColors |= army1.GetColor(); + _autoCombatColors |= army1.GetColor(); } if ( army2.isControlHuman() ) { - _autoBattleColors |= army2.GetColor(); + _autoCombatColors |= army2.GetColor(); } } @@ -484,8 +483,8 @@ void Battle::Arena::UnitTurn( const Units & orderHistory ) } if ( !actions.empty() ) { - // Pending actions from the user interface (such as toggling auto battle) have "already occurred" and - // therefore should be handled first, before any other actions. Just skip the rest of the branches. + // Pending actions from the user interface (such as toggling the auto combat on/off) have "already occurred" + // and therefore should be handled first, before any other actions. Just skip the rest of the branches. } else if ( _currentUnit->GetSpeed() == Speed::STANDING ) { // Unit has either finished its turn, is dead, or has become immovable due to some spell. Even if the @@ -506,7 +505,7 @@ void Battle::Arena::UnitTurn( const Units & orderHistory ) _bridge->SetPassability( *_currentUnit ); } - if ( ( _currentUnit->GetCurrentControl() & CONTROL_AI ) || ( _currentUnit->GetCurrentColor() & _autoBattleColors ) ) { + if ( ( _currentUnit->GetCurrentControl() & CONTROL_AI ) || ( _currentUnit->GetCurrentColor() & _autoCombatColors ) ) { AI::BattlePlanner::Get().BattleTurn( *this, *_currentUnit, actions ); } else { @@ -1432,14 +1431,14 @@ Battle::Result & Battle::Arena::GetResult() return result_game; } -bool Battle::Arena::AutoBattleInProgress() const +bool Battle::Arena::AutoCombatInProgress() const { if ( _currentUnit == nullptr ) { return false; } - if ( _autoBattleColors & GetCurrentColor() ) { - // Auto battle mode cannot be enabled for a player controlled by AI + if ( _autoCombatColors & GetCurrentColor() ) { + // Auto combat mode cannot be enabled for a player controlled by the AI assert( !( GetCurrentForce().GetControl() & CONTROL_AI ) ); return true; @@ -1448,7 +1447,7 @@ bool Battle::Arena::AutoBattleInProgress() const return false; } -bool Battle::Arena::EnemyOfAIHasAutoBattleInProgress() const +bool Battle::Arena::EnemyOfAIHasAutoCombatInProgress() const { if ( _currentUnit == nullptr ) { return false; @@ -1464,10 +1463,10 @@ bool Battle::Arena::EnemyOfAIHasAutoBattleInProgress() const return false; } - return ( _autoBattleColors & enemyForce.GetColor() ); + return ( _autoCombatColors & enemyForce.GetColor() ); } -bool Battle::Arena::CanToggleAutoBattle() const +bool Battle::Arena::CanToggleAutoCombat() const { if ( _currentUnit == nullptr ) { return false; diff --git a/src/fheroes2/battle/battle_arena.h b/src/fheroes2/battle/battle_arena.h index aecd1edba22..4306c97098b 100644 --- a/src/fheroes2/battle/battle_arena.h +++ b/src/fheroes2/battle/battle_arena.h @@ -104,9 +104,9 @@ namespace Battle void Turns(); bool BattleValid() const; - bool AutoBattleInProgress() const; - bool EnemyOfAIHasAutoBattleInProgress() const; - bool CanToggleAutoBattle() const; + bool AutoCombatInProgress() const; + bool EnemyOfAIHasAutoCombatInProgress() const; + bool CanToggleAutoCombat() const; uint32_t GetTurnNumber() const { @@ -284,8 +284,8 @@ namespace Battle void ApplyActionSpellCast( Command & cmd ); void ApplyActionTower( Command & cmd ); void ApplyActionCatapult( Command & cmd ); - void ApplyActionAutoSwitch( Command & cmd ); - void ApplyActionAutoFinish( const Command & cmd ); + void ApplyActionToggleAutoCombat( Command & cmd ); + void ApplyActionQuickCombat( const Command & cmd ); void ApplyActionSpellSummonElemental( const Command & cmd, const Spell & spell ); void ApplyActionSpellMirrorImage( Command & cmd ); @@ -343,8 +343,8 @@ namespace Battle int _covrIcnId{ ICN::UNKNOWN }; uint32_t _turnNumber{ 0 }; - // A set of colors of players for whom the auto-battle mode is enabled - int _autoBattleColors{ 0 }; + // A set of colors of players for whom the auto combat mode is enabled + int _autoCombatColors{ 0 }; // This random number generator should only be used in code that is equally used by both AI and the human // player - that is, in code related to the processing of battle commands. It cannot be safely used in other diff --git a/src/fheroes2/battle/battle_command.cpp b/src/fheroes2/battle/battle_command.cpp index 45e7d307a8b..64313254528 100644 --- a/src/fheroes2/battle/battle_command.cpp +++ b/src/fheroes2/battle/battle_command.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2019 - 2023 * + * Copyright (C) 2019 - 2025 * * * * Free Heroes2 Engine: http://sourceforge.net/projects/fheroes2 * * Copyright (C) 2012 by Andrey Afletdinov * @@ -63,8 +63,8 @@ uint32_t Battle::Command::updateSeed( uint32_t seed ) const break; // These commands should never affect the seed generation - case CommandType::AUTO_SWITCH: - case CommandType::AUTO_FINISH: + case CommandType::TOGGLE_AUTO_COMBAT: + case CommandType::QUICK_COMBAT: break; default: diff --git a/src/fheroes2/battle/battle_command.h b/src/fheroes2/battle/battle_command.h index fa681d1139a..124c8f7cf95 100644 --- a/src/fheroes2/battle/battle_command.h +++ b/src/fheroes2/battle/battle_command.h @@ -46,8 +46,8 @@ namespace Battle RETREAT, SURRENDER, SKIP, - AUTO_SWITCH, - AUTO_FINISH + TOGGLE_AUTO_COMBAT, + QUICK_COMBAT }; class Command final : public std::vector @@ -62,8 +62,8 @@ namespace Battle static constexpr std::integral_constant RETREAT{}; static constexpr std::integral_constant SURRENDER{}; static constexpr std::integral_constant SKIP{}; - static constexpr std::integral_constant AUTO_SWITCH{}; - static constexpr std::integral_constant AUTO_FINISH{}; + static constexpr std::integral_constant TOGGLE_AUTO_COMBAT{}; + static constexpr std::integral_constant QUICK_COMBAT{}; template explicit Command( std::integral_constant /* tag */, const Types... params ) @@ -107,7 +107,7 @@ namespace Battle // UID static_assert( sizeof...( params ) == 1 ); } - else if constexpr ( cmd == CommandType::AUTO_SWITCH ) { + else if constexpr ( cmd == CommandType::TOGGLE_AUTO_COMBAT ) { // Color static_assert( sizeof...( params ) == 1 ); } diff --git a/src/fheroes2/battle/battle_interface.cpp b/src/fheroes2/battle/battle_interface.cpp index 482f39556df..7b940605295 100644 --- a/src/fheroes2/battle/battle_interface.cpp +++ b/src/fheroes2/battle/battle_interface.cpp @@ -2693,10 +2693,10 @@ int Battle::Interface::GetBattleSpellCursor( std::string & statusMsg ) const void Battle::Interface::getPendingActions( Actions & actions ) { - if ( _interruptAutoBattleForColor ) { - actions.emplace_back( Command::AUTO_SWITCH, _interruptAutoBattleForColor ); + if ( _interruptAutoCombatForColor ) { + actions.emplace_back( Command::TOGGLE_AUTO_COMBAT, _interruptAutoCombatForColor ); - _interruptAutoBattleForColor = 0; + _interruptAutoCombatForColor = 0; } } @@ -2801,13 +2801,13 @@ void Battle::Interface::HumanBattleTurn( const Unit & unit, Actions & actions, s else if ( Game::HotKeyPressEvent( Game::HotKeyEvent::BATTLE_OPTIONS ) ) { EventShowOptions(); } - // Switch the auto battle mode on - else if ( Game::HotKeyPressEvent( Game::HotKeyEvent::BATTLE_AUTO_SWITCH ) ) { - EventStartAutoBattle( unit, actions ); + // Switch the auto combat mode on + else if ( Game::HotKeyPressEvent( Game::HotKeyEvent::BATTLE_TOGGLE_AUTO_COMBAT ) ) { + EventStartAutoCombat( unit, actions ); } - // Finish the battle in auto mode - else if ( Game::HotKeyPressEvent( Game::HotKeyEvent::BATTLE_AUTO_FINISH ) ) { - EventAutoFinish( actions ); + // Resolve the combat in quick combat mode + else if ( Game::HotKeyPressEvent( Game::HotKeyEvent::BATTLE_QUICK_COMBAT ) ) { + EventQuickCombat( actions ); } // Cast the spell else if ( Game::HotKeyPressEvent( Game::HotKeyEvent::BATTLE_CAST_SPELL ) ) { @@ -2872,13 +2872,14 @@ void Battle::Interface::HumanBattleTurn( const Unit & unit, Actions & actions, s else if ( le.isMouseCursorPosInArea( _buttonAuto.area() ) ) { cursor.SetThemes( Cursor::WAR_POINTER ); - msg = _( "Enable auto combat" ); + msg = _( "Automatic combat modes" ); if ( le.MouseClickLeft( _buttonAuto.area() ) ) { - EventStartAutoBattle( unit, actions ); + OpenAutoModeDialog( unit, actions ); } else if ( le.isMouseRightButtonPressed() ) { - fheroes2::showStandardTextMessage( _( "Auto Combat" ), _( "Allows the computer to fight out the battle for you." ), Dialog::ZERO ); + fheroes2::showStandardTextMessage( _( "Automatic Combat Modes" ), _( "Choose between proceeding the combat in auto combat mode or in quick combat mode." ), + Dialog::ZERO ); } } else if ( le.isMouseCursorPosInArea( _buttonSettings.area() ) ) { @@ -3195,33 +3196,117 @@ void Battle::Interface::EventShowOptions() humanturn_redraw = true; } -void Battle::Interface::EventStartAutoBattle( const Unit & unit, Actions & actions ) +bool Battle::Interface::EventStartAutoCombat( const Unit & unit, Actions & actions ) { // TODO: remove these temporary assertions - assert( arena.CanToggleAutoBattle() ); - assert( !arena.AutoBattleInProgress() ); + assert( arena.CanToggleAutoCombat() ); + assert( !arena.AutoCombatInProgress() ); - int startAutoBattle = fheroes2::showStandardTextMessage( {}, _( "Are you sure you want to enable auto combat?" ), Dialog::YES | Dialog::NO ); - if ( startAutoBattle != Dialog::YES ) { - return; + if ( fheroes2::showStandardTextMessage( {}, _( "Are you sure you want to enable the auto combat mode?" ), Dialog::YES | Dialog::NO ) != Dialog::YES ) { + return false; } - actions.emplace_back( Command::AUTO_SWITCH, unit.GetCurrentOrArmyColor() ); + actions.emplace_back( Command::TOGGLE_AUTO_COMBAT, unit.GetCurrentOrArmyColor() ); humanturn_redraw = true; humanturn_exit = true; + + return true; } -void Battle::Interface::EventAutoFinish( Actions & actions ) +bool Battle::Interface::EventQuickCombat( Actions & actions ) { - if ( fheroes2::showStandardTextMessage( {}, _( "Are you sure you want to finish the battle in auto mode?" ), Dialog::YES | Dialog::NO ) != Dialog::YES ) { - return; + if ( fheroes2::showStandardTextMessage( {}, _( "Are you sure you want to resolve the battle in the quick combat mode?" ), Dialog::YES | Dialog::NO ) + != Dialog::YES ) { + return false; } - actions.emplace_back( Command::AUTO_FINISH ); + actions.emplace_back( Command::QUICK_COMBAT ); humanturn_redraw = true; humanturn_exit = true; + + return true; +} + +void Battle::Interface::OpenAutoModeDialog( const Unit & unit, Actions & actions ) +{ + Cursor::Get().SetThemes( Cursor::POINTER ); + + const bool isEvilInterface = Settings::Get().isEvilInterfaceEnabled(); + + const int autoCombatButtonICN = isEvilInterface ? ICN::BUTTON_AUTO_COMBAT_EVIL : ICN::BUTTON_AUTO_COMBAT_GOOD; + const int quickCombatButtonICN = isEvilInterface ? ICN::BUTTON_QUICK_COMBAT_EVIL : ICN::BUTTON_QUICK_COMBAT_GOOD; + const int cancelButtonICN = isEvilInterface ? ICN::BUTTON_SMALL_CANCEL_EVIL : ICN::BUTTON_SMALL_CANCEL_GOOD; + + const fheroes2::Sprite & quickCombatButtonReleased = fheroes2::AGG::GetICN( quickCombatButtonICN, 0 ); + const fheroes2::Sprite & autoCombatButtonReleased = fheroes2::AGG::GetICN( autoCombatButtonICN, 0 ); + const fheroes2::Sprite & cancelButtonReleased = fheroes2::AGG::GetICN( cancelButtonICN, 0 ); + + const int32_t autoButtonsXOffset = 20; + const int32_t autoButtonsYOffset = 15; + const int32_t titleYOffset = 16; + const int32_t buttonSeparation = 37; + + const fheroes2::Text title( _( "Automatic Combat Modes" ), { fheroes2::FontSize::NORMAL, fheroes2::FontColor::YELLOW } ); + + const int32_t backgroundWidth = autoButtonsXOffset * 2 + quickCombatButtonReleased.width() + buttonSeparation + autoCombatButtonReleased.width(); + const int32_t backgroundHeight + = titleYOffset + title.height( backgroundWidth ) + quickCombatButtonReleased.height() + cancelButtonReleased.height() + autoButtonsYOffset + 28; + + fheroes2::Display & display = fheroes2::Display::instance(); + fheroes2::StandardWindow background( backgroundWidth, backgroundHeight, true, display ); + + fheroes2::Button buttonAutoCombat; + fheroes2::Button buttonQuickCombat; + fheroes2::Button buttonCancel; + + background.renderButton( buttonAutoCombat, isEvilInterface ? ICN::BUTTON_AUTO_COMBAT_EVIL : ICN::BUTTON_AUTO_COMBAT_GOOD, 0, 1, { autoButtonsXOffset, 0 }, + fheroes2::StandardWindow::Padding::CENTER_LEFT ); + background.renderButton( buttonQuickCombat, isEvilInterface ? ICN::BUTTON_QUICK_COMBAT_EVIL : ICN::BUTTON_QUICK_COMBAT_GOOD, 0, 1, { autoButtonsXOffset, 0 }, + fheroes2::StandardWindow::Padding::CENTER_RIGHT ); + background.renderButton( buttonCancel, isEvilInterface ? ICN::BUTTON_SMALL_CANCEL_EVIL : ICN::BUTTON_SMALL_CANCEL_GOOD, 0, 1, { 0, 11 }, + fheroes2::StandardWindow::Padding::BOTTOM_CENTER ); + + const fheroes2::Rect roiArea = background.activeArea(); + title.draw( roiArea.x, roiArea.y + titleYOffset, backgroundWidth, display ); + + display.render( background.totalArea() ); + LocalEvent & le = LocalEvent::Get(); + + while ( le.HandleEvents() ) { + buttonAutoCombat.drawOnState( le.isMouseLeftButtonPressedInArea( buttonAutoCombat.area() ) ); + buttonQuickCombat.drawOnState( le.isMouseLeftButtonPressedInArea( buttonQuickCombat.area() ) ); + buttonCancel.drawOnState( le.isMouseLeftButtonPressedInArea( buttonCancel.area() ) ); + + if ( le.MouseClickLeft( buttonCancel.area() ) || Game::HotKeyPressEvent( Game::HotKeyEvent::DEFAULT_CANCEL ) ) { + return; + } + if ( ( le.MouseClickLeft( buttonAutoCombat.area() ) || Game::HotKeyPressEvent( Game::HotKeyEvent::BATTLE_TOGGLE_AUTO_COMBAT ) ) + && EventStartAutoCombat( unit, actions ) ) { + return; + } + if ( ( le.MouseClickLeft( buttonQuickCombat.area() ) || Game::HotKeyPressEvent( Game::HotKeyEvent::BATTLE_QUICK_COMBAT ) ) && EventQuickCombat( actions ) ) { + return; + } + + if ( le.isMouseRightButtonPressedInArea( buttonCancel.area() ) ) { + fheroes2::showStandardTextMessage( _( "Cancel" ), _( "Exit this menu." ), Dialog::ZERO ); + } + else if ( le.isMouseRightButtonPressedInArea( buttonAutoCombat.area() ) ) { + std::string msg = _( "The computer continues the combat for you." ); + msg += "\n\n"; + msg += _( + "autoCombat|This can be interrupted at any moment by pressing the Auto Combat hotkey or the default Cancel key, or by performing a left or right click anywhere on the game screen." ); + fheroes2::showStandardTextMessage( _( "Auto Combat" ), msg, Dialog::ZERO ); + } + else if ( le.isMouseRightButtonPressedInArea( buttonQuickCombat.area() ) ) { + std::string msg = _( "The combat is resolved from the current state." ); + msg += "\n\n"; + msg += _( "quickCombat|This cannot be undone." ); + fheroes2::showStandardTextMessage( _( "Quick Combat" ), msg, Dialog::ZERO ); + } + } } void Battle::Interface::MousePressRightBoardAction( const Cell & cell ) const @@ -6419,39 +6504,39 @@ void Battle::Interface::CheckGlobalEvents( LocalEvent & le ) } } - // Check if auto battle interruption was requested. - InterruptAutoBattleIfRequested( le ); + // Check if auto combat interruption was requested. + InterruptAutoCombatIfRequested( le ); } -void Battle::Interface::InterruptAutoBattleIfRequested( LocalEvent & le ) +void Battle::Interface::InterruptAutoCombatIfRequested( LocalEvent & le ) { // Interrupt only if automation is currently on. - if ( !arena.AutoBattleInProgress() && !arena.EnemyOfAIHasAutoBattleInProgress() ) { + if ( !arena.AutoCombatInProgress() && !arena.EnemyOfAIHasAutoCombatInProgress() ) { return; } - if ( !le.MouseClickLeft() && !le.MouseClickRight() && !Game::HotKeyPressEvent( Game::HotKeyEvent::BATTLE_AUTO_SWITCH ) + if ( !le.MouseClickLeft() && !le.MouseClickRight() && !Game::HotKeyPressEvent( Game::HotKeyEvent::BATTLE_TOGGLE_AUTO_COMBAT ) && !Game::HotKeyPressEvent( Game::HotKeyEvent::DEFAULT_CANCEL ) ) { return; } - // Identify which color requested the auto battle interrupt. + // Identify which color requested the auto combat interruption. int color = arena.GetCurrentColor(); if ( arena.GetCurrentForce().GetControl() & CONTROL_AI ) { color = arena.GetOppositeColor( color ); } // The battle interruption is already scheduled, no need for the dialog. - if ( color == _interruptAutoBattleForColor ) { + if ( color == _interruptAutoCombatForColor ) { return; } - // Right now there should be no pending auto battle interruptions. - assert( _interruptAutoBattleForColor == 0 ); + // Right now there should be no pending auto combat interruptions. + assert( _interruptAutoCombatForColor == 0 ); - const int interrupt = fheroes2::showStandardTextMessage( {}, _( "Are you sure you want to interrupt the auto combat?" ), Dialog::YES | Dialog::NO ); + const int interrupt = fheroes2::showStandardTextMessage( {}, _( "Are you sure you want to interrupt the auto combat mode?" ), Dialog::YES | Dialog::NO ); if ( interrupt == Dialog::YES ) { - _interruptAutoBattleForColor = color; + _interruptAutoCombatForColor = color; } } diff --git a/src/fheroes2/battle/battle_interface.h b/src/fheroes2/battle/battle_interface.h index a72d5383d88..b1854bf5b78 100644 --- a/src/fheroes2/battle/battle_interface.h +++ b/src/fheroes2/battle/battle_interface.h @@ -414,14 +414,15 @@ namespace Battle void SwitchAllUnitsAnimation( const int32_t animationState ) const; void UpdateContourColor(); void CheckGlobalEvents( LocalEvent & ); - void InterruptAutoBattleIfRequested( LocalEvent & le ); + void InterruptAutoCombatIfRequested( LocalEvent & le ); void SetHeroAnimationReactionToTroopDeath( const int32_t deathColor ) const; void ProcessingHeroDialogResult( const int result, Actions & actions ); void _openBattleSettingsDialog(); - void EventStartAutoBattle( const Unit & unit, Actions & actions ); - void EventAutoFinish( Actions & actions ); + bool EventStartAutoCombat( const Unit & unit, Actions & actions ); + bool EventQuickCombat( Actions & actions ); + void OpenAutoModeDialog( const Unit & unit, Actions & actions ); void EventShowOptions(); void MouseLeftClickBoardAction( const int themes, const Cell & cell, const bool isConfirmed, Actions & actions ); void MousePressRightBoardAction( const Cell & cell ) const; @@ -458,7 +459,7 @@ namespace Battle uint32_t animation_flags_frame{ 0 }; int catapult_frame{ 0 }; - int _interruptAutoBattleForColor{ 0 }; + int _interruptAutoCombatForColor{ 0 }; // The Channel ID of pre-battle sound. Used to check it is over to start the battle music. std::optional _preBattleSoundChannelId{ -1 }; diff --git a/src/fheroes2/dialog/dialog_system_options.cpp b/src/fheroes2/dialog/dialog_system_options.cpp index ce0d77511a8..093451a348a 100644 --- a/src/fheroes2/dialog/dialog_system_options.cpp +++ b/src/fheroes2/dialog/dialog_system_options.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2021 - 2024 * + * Copyright (C) 2021 - 2025 * * * * 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 * @@ -191,7 +191,7 @@ namespace } else { const fheroes2::Sprite & autoBattleIcon = fheroes2::AGG::GetICN( ICN::SPANEL, 18 ); - fheroes2::drawOption( optionRoi, autoBattleIcon, _( "Battles" ), _( "autoBattle|Manual" ), fheroes2::UiOptionTextWidth::THREE_ELEMENTS_ROW ); + fheroes2::drawOption( optionRoi, autoBattleIcon, _( "Battles" ), _( "combatMode|Manual" ), fheroes2::UiOptionTextWidth::THREE_ELEMENTS_ROW ); } } diff --git a/src/fheroes2/game/game_hotkeys.cpp b/src/fheroes2/game/game_hotkeys.cpp index b9505480a5a..106d0976884 100644 --- a/src/fheroes2/game/game_hotkeys.cpp +++ b/src/fheroes2/game/game_hotkeys.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -274,10 +275,10 @@ namespace = { Game::HotKeyCategory::BATTLE, gettext_noop( "hotkey|retreat from battle" ), fheroes2::Key::KEY_R }; hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::BATTLE_SURRENDER )] = { Game::HotKeyCategory::BATTLE, gettext_noop( "hotkey|surrender during battle" ), fheroes2::Key::KEY_S }; - hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::BATTLE_AUTO_SWITCH )] - = { Game::HotKeyCategory::BATTLE, gettext_noop( "hotkey|toggle battle auto mode" ), fheroes2::Key::KEY_A }; - hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::BATTLE_AUTO_FINISH )] - = { Game::HotKeyCategory::BATTLE, gettext_noop( "hotkey|finish the battle in auto mode" ), fheroes2::Key::KEY_Q }; + hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::BATTLE_TOGGLE_AUTO_COMBAT )] + = { Game::HotKeyCategory::BATTLE, gettext_noop( "hotkey|toggle auto combat mode" ), fheroes2::Key::KEY_A }; + hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::BATTLE_QUICK_COMBAT )] + = { Game::HotKeyCategory::BATTLE, gettext_noop( "hotkey|quick combat" ), fheroes2::Key::KEY_Q }; hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::BATTLE_OPTIONS )] = { Game::HotKeyCategory::BATTLE, gettext_noop( "hotkey|battle options" ), fheroes2::Key::KEY_O }; hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::BATTLE_SKIP )] @@ -437,7 +438,22 @@ void Game::HotKeysLoad( const std::string & filename ) const char * eventName = _( hotKeyEventInfo[eventId].name ); std::string value = config.StrParams( eventName ); if ( value.empty() ) { - continue; + // TODO: remove this temporary workaround + if ( eventName == std::string_view( "toggle auto combat mode" ) ) { + value = config.StrParams( "toggle battle auto mode" ); + if ( value.empty() ) { + continue; + } + } + else if ( eventName == std::string_view( "quick combat" ) ) { + value = config.StrParams( "finish the battle in auto mode" ); + if ( value.empty() ) { + continue; + } + } + else { + continue; + } } value = StringUpper( value ); diff --git a/src/fheroes2/game/game_hotkeys.h b/src/fheroes2/game/game_hotkeys.h index a5b794d4ccf..155473b6269 100644 --- a/src/fheroes2/game/game_hotkeys.h +++ b/src/fheroes2/game/game_hotkeys.h @@ -141,8 +141,8 @@ namespace Game BATTLE_RETREAT, BATTLE_SURRENDER, - BATTLE_AUTO_SWITCH, - BATTLE_AUTO_FINISH, + BATTLE_TOGGLE_AUTO_COMBAT, + BATTLE_QUICK_COMBAT, BATTLE_OPTIONS, BATTLE_SKIP, BATTLE_CAST_SPELL, diff --git a/src/fheroes2/gui/ui_button.cpp b/src/fheroes2/gui/ui_button.cpp index d2476b980af..3022688f679 100644 --- a/src/fheroes2/gui/ui_button.cpp +++ b/src/fheroes2/gui/ui_button.cpp @@ -174,6 +174,29 @@ namespace return output; } + void addButtonShine( fheroes2::Sprite & buttonImage, const int emptyButtonIcnID ) + { + const bool isGoodButton = ( emptyButtonIcnID == ICN::EMPTY_GOOD_BUTTON ); + if ( isGoodButton || emptyButtonIcnID == ICN::EMPTY_EVIL_BUTTON ) { + const uint8_t firstColor = 10; + const uint8_t secondColor = isGoodButton ? 38 : 15; + const uint8_t lastColor = isGoodButton ? 39 : 16; + // Left-side shine + fheroes2::SetPixel( buttonImage, 11, 4, firstColor ); + fheroes2::SetPixel( buttonImage, 13, 4, firstColor ); + fheroes2::SetPixel( buttonImage, 9, 6, firstColor ); + fheroes2::SetPixel( buttonImage, 10, 5, secondColor ); + fheroes2::SetPixel( buttonImage, 12, 5, secondColor ); + fheroes2::SetPixel( buttonImage, 8, 7, lastColor ); + fheroes2::SetPixel( buttonImage, 15, 4, lastColor ); + // Right-side shine + fheroes2::SetPixel( buttonImage, buttonImage.width() - 9, 4, firstColor ); + fheroes2::SetPixel( buttonImage, buttonImage.width() - 7, 4, firstColor ); + fheroes2::DrawLine( buttonImage, { buttonImage.width() - 10, 5 }, { buttonImage.width() - 11, 6 }, secondColor ); + fheroes2::SetPixel( buttonImage, buttonImage.width() - 8, 5, secondColor ); + } + } + void getButtonSpecificValues( const int emptyButtonIcnID, fheroes2::FontColor & font, fheroes2::Point & textAreaBorders, fheroes2::Size & minimumTextArea, fheroes2::Size & maximumTextArea, fheroes2::Size & backgroundBorders, fheroes2::Point & releasedOffset, fheroes2::Point & pressedOffset ) @@ -745,7 +768,6 @@ namespace fheroes2 const fheroes2::Text pressedText( supportedText, { fheroes2::FontSize::BUTTON_PRESSED, buttonFont } ); // We need to pass an argument to width() so that it correctly accounts for multi-lined texts. - // TODO: Remove the need for the argument once width() has been improved to handle this. const int32_t textWidth = releasedText.width( maximumTextArea.width ); assert( textWidth > 0 ); @@ -757,6 +779,10 @@ namespace fheroes2 const int32_t textHeight = releasedText.height( textAreaWidth ); assert( textHeight > 0 ); + // Add extra y-margin for multi-lined texts on normal buttons. + if ( ( emptyButtonIcnID == ICN::EMPTY_EVIL_BUTTON || emptyButtonIcnID == ICN::EMPTY_GOOD_BUTTON ) && textHeight > 17 ) { + textAreaMargins.y += 16; + } const int32_t borderedTextHeight = textHeight + textAreaMargins.y; const int32_t textAreaHeight = std::clamp( borderedTextHeight, minimumTextArea.height, maximumTextArea.height ); @@ -768,6 +794,9 @@ namespace fheroes2 if ( buttonBackgroundIcnID != ICN::UNKNOWN ) { makeTransparentBackground( released, pressed, buttonBackgroundIcnID ); } + if ( emptyButtonIcnID == ICN::EMPTY_EVIL_BUTTON || emptyButtonIcnID == ICN::EMPTY_GOOD_BUTTON ) { + addButtonShine( released, emptyButtonIcnID ); + } const fheroes2::Size releasedTextSize( releasedText.width( textAreaWidth ), releasedText.height( textAreaWidth ) ); const fheroes2::Size pressedTextSize( pressedText.width( textAreaWidth ), pressedText.height( textAreaWidth ) );