diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
new file mode 100644
index 00000000000..dd84ea7824f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000000..bbcbbe7d615
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/VisualStudio/fheroes2/sources.props b/VisualStudio/fheroes2/sources.props
index 242180795d3..7317defda60 100644
--- a/VisualStudio/fheroes2/sources.props
+++ b/VisualStudio/fheroes2/sources.props
@@ -118,6 +118,7 @@
+
@@ -328,6 +329,7 @@
+
diff --git a/src/fheroes2/editor/editor_castle_details_window.cpp b/src/fheroes2/editor/editor_castle_details_window.cpp
index cf2e551148a..1403e8f9ae1 100644
--- a/src/fheroes2/editor/editor_castle_details_window.cpp
+++ b/src/fheroes2/editor/editor_castle_details_window.cpp
@@ -136,7 +136,7 @@ namespace
{
LocalEvent & le = LocalEvent::Get();
- if ( le.MouseClickLeft() ) {
+ if ( le.MouseClickLeft( _area ) ) {
if ( restrictionMode ) {
if ( _restrictedId <= -1 ) {
_restrictedId = _buildingVariants;
@@ -162,7 +162,7 @@ namespace
return true;
}
- if ( le.isMouseRightButtonPressed() ) {
+ if ( le.isMouseRightButtonPressedInArea( _area ) ) {
const BuildingType building = _getBuildindTypeForRender();
std::string description = BuildingInfo::getBuildingDescription( _race, building );
const std::string requirement = fheroes2::getBuildingRequirementString( _race, building );
@@ -485,7 +485,7 @@ namespace Editor
message = _( "Click to change the Castle name. Right-click to reset to default." );
bool redrawName = false;
- if ( le.MouseClickLeft() ) {
+ if ( le.MouseClickLeft( nameArea ) ) {
std::string res = castleMetadata.customName;
// TODO: use the provided language to set the castle's name.
@@ -497,7 +497,7 @@ namespace Editor
redrawName = true;
}
}
- else if ( le.MouseClickRight() ) {
+ else if ( le.MouseClickRight( nameArea ) ) {
castleMetadata.customName.clear();
redrawName = true;
}
@@ -512,18 +512,18 @@ namespace Editor
else if ( isTown && le.isMouseCursorPosInArea( allowCastleArea ) ) {
message = _( "Allow to build a castle in this town." );
- if ( le.MouseClickLeft() ) {
+ if ( le.MouseClickLeft( allowCastleArea ) ) {
allowCastleSign.isHidden() ? allowCastleSign.show() : allowCastleSign.hide();
display.render( allowCastleSign.getArea() );
}
- else if ( le.isMouseRightButtonPressed() ) {
+ else if ( le.isMouseRightButtonPressedInArea( allowCastleArea ) ) {
fheroes2::showStandardTextMessage( _( "Allow Castle build" ), message, Dialog::ZERO );
}
}
else if ( le.isMouseCursorPosInArea( defaultBuildingsArea ) ) {
message = _( "Toggle the use of default buildings. Custom buildings will be reset!" );
- if ( le.MouseClickLeft() ) {
+ if ( le.MouseClickLeft( defaultBuildingsArea ) ) {
if ( defaultBuildingsSign.isHidden() ) {
// Reset all buildings to their build and restrict default states.
for ( BuildingData & building : buildings ) {
@@ -544,7 +544,7 @@ namespace Editor
display.render( dialogRoi );
}
}
- else if ( le.isMouseRightButtonPressed() ) {
+ else if ( le.isMouseRightButtonPressedInArea( defaultBuildingsArea ) ) {
fheroes2::showStandardTextMessage( _( "Default Buildings" ), message, Dialog::ZERO );
}
}
@@ -552,7 +552,7 @@ namespace Editor
else if ( le.isMouseCursorPosInArea( buttonRestrictBuilding.area() ) ) {
message = _( "Toggle building construction restriction mode." );
- if ( le.isMouseRightButtonPressed() ) {
+ if ( le.isMouseRightButtonPressedInArea( buttonRestrictBuilding.area() ) ) {
fheroes2::showStandardTextMessage( _( "Restrict Building Construction" ), message, Dialog::ZERO );
}
}
@@ -560,7 +560,7 @@ namespace Editor
else if ( isNeutral && le.isMouseCursorPosInArea( defaultArmyArea ) ) {
message = _( "Use default defenders army." );
- if ( le.MouseClickLeft() ) {
+ if ( le.MouseClickLeft( defaultArmyArea ) ) {
if ( defaultArmySign.isHidden() ) {
defaultArmySign.show();
castleArmy.Reset( false );
@@ -572,7 +572,7 @@ namespace Editor
display.render( defaultArmySign.getArea() );
}
}
- else if ( le.isMouseRightButtonPressed() ) {
+ else if ( le.isMouseRightButtonPressedInArea( defaultArmyArea ) ) {
fheroes2::showStandardTextMessage( _( "Default Army" ), message, Dialog::ZERO );
}
}
@@ -591,7 +591,7 @@ namespace Editor
else if ( le.isMouseCursorPosInArea( buttonExit.area() ) ) {
message = _( "Exit Castle Options" );
- if ( le.isMouseRightButtonPressed() ) {
+ if ( le.isMouseRightButtonPressedInArea( buttonExit.area() ) ) {
fheroes2::showStandardTextMessage( _( "Exit" ), message, Dialog::ZERO );
}
}
diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp
index 04a671f0687..277191486e2 100644
--- a/src/fheroes2/editor/editor_interface.cpp
+++ b/src/fheroes2/editor/editor_interface.cpp
@@ -44,6 +44,7 @@
#include "editor_map_specs_window.h"
#include "editor_object_popup_window.h"
#include "editor_save_map_window.h"
+#include "editor_secondary_skill_selection.h"
#include "editor_spell_selection.h"
#include "editor_sphinx_window.h"
#include "game.h"
@@ -352,7 +353,7 @@ namespace
mapFormat.adventureMapEventMetadata.erase( objectIter->id );
break;
case MP2::OBJ_PYRAMID:
- mapFormat.spellObjectMetadata.erase( objectIter->id );
+ mapFormat.selectionObjectMetadata.erase( objectIter->id );
break;
case MP2::OBJ_SIGN:
assert( mapFormat.signMetadata.find( objectIter->id ) != mapFormat.signMetadata.end() );
@@ -362,6 +363,9 @@ namespace
assert( mapFormat.sphinxMetadata.find( objectIter->id ) != mapFormat.sphinxMetadata.end() );
mapFormat.sphinxMetadata.erase( objectIter->id );
break;
+ case MP2::OBJ_WITCHS_HUT:
+ mapFormat.selectionObjectMetadata.erase( objectIter->id );
+ break;
default:
break;
}
@@ -424,7 +428,7 @@ namespace
case MP2::OBJ_SHRINE_SECOND_CIRCLE:
case MP2::OBJ_SHRINE_THIRD_CIRCLE:
// We cannot assert non-existing metadata as these objects could have been created by an older Editor version.
- mapFormat.spellObjectMetadata.erase( objectIter->id );
+ mapFormat.selectionObjectMetadata.erase( objectIter->id );
break;
default:
break;
@@ -1410,11 +1414,11 @@ namespace Interface
}
}
else if ( objectType == MP2::OBJ_SHRINE_FIRST_CIRCLE || objectType == MP2::OBJ_SHRINE_SECOND_CIRCLE || objectType == MP2::OBJ_SHRINE_THIRD_CIRCLE ) {
- if ( _mapFormat.spellObjectMetadata.find( object.id ) == _mapFormat.spellObjectMetadata.end() ) {
- _mapFormat.spellObjectMetadata[object.id] = {};
+ if ( _mapFormat.selectionObjectMetadata.find( object.id ) == _mapFormat.selectionObjectMetadata.end() ) {
+ _mapFormat.selectionObjectMetadata[object.id] = {};
}
- auto & originalMetadata = _mapFormat.spellObjectMetadata[object.id];
+ auto & originalMetadata = _mapFormat.selectionObjectMetadata[object.id];
auto newMetadata = originalMetadata;
int spellLevel = 0;
@@ -1432,23 +1436,38 @@ namespace Interface
spellLevel = 1;
}
- if ( Editor::openSpellSelectionWindow( MP2::StringObject( objectType ), spellLevel, newMetadata.allowedSpells )
- && originalMetadata.allowedSpells != newMetadata.allowedSpells ) {
+ if ( Editor::openSpellSelectionWindow( MP2::StringObject( objectType ), spellLevel, newMetadata.selectedItems )
+ && originalMetadata.selectedItems != newMetadata.selectedItems ) {
+ fheroes2::ActionCreator action( _historyManager, _mapFormat );
+ originalMetadata = std::move( newMetadata );
+ action.commit();
+ }
+ }
+ else if ( objectType == MP2::OBJ_WITCHS_HUT ) {
+ if ( _mapFormat.selectionObjectMetadata.find( object.id ) == _mapFormat.selectionObjectMetadata.end() ) {
+ _mapFormat.selectionObjectMetadata[object.id] = {};
+ }
+
+ auto & originalMetadata = _mapFormat.selectionObjectMetadata[object.id];
+ auto newMetadata = originalMetadata;
+
+ if ( Editor::openSecondarySkillSelectionWindow( MP2::StringObject( objectType ), 1, newMetadata.selectedItems )
+ && originalMetadata.selectedItems != newMetadata.selectedItems ) {
fheroes2::ActionCreator action( _historyManager, _mapFormat );
originalMetadata = std::move( newMetadata );
action.commit();
}
}
else if ( objectType == MP2::OBJ_PYRAMID ) {
- if ( _mapFormat.spellObjectMetadata.find( object.id ) == _mapFormat.spellObjectMetadata.end() ) {
- _mapFormat.spellObjectMetadata[object.id] = {};
+ if ( _mapFormat.selectionObjectMetadata.find( object.id ) == _mapFormat.selectionObjectMetadata.end() ) {
+ _mapFormat.selectionObjectMetadata[object.id] = {};
}
- auto & originalMetadata = _mapFormat.spellObjectMetadata[object.id];
+ auto & originalMetadata = _mapFormat.selectionObjectMetadata[object.id];
auto newMetadata = originalMetadata;
- if ( Editor::openSpellSelectionWindow( MP2::StringObject( objectType ), 5, newMetadata.allowedSpells )
- && originalMetadata.allowedSpells != newMetadata.allowedSpells ) {
+ if ( Editor::openSpellSelectionWindow( MP2::StringObject( objectType ), 5, newMetadata.selectedItems )
+ && originalMetadata.selectedItems != newMetadata.selectedItems ) {
fheroes2::ActionCreator action( _historyManager, _mapFormat );
originalMetadata = std::move( newMetadata );
action.commit();
@@ -2135,7 +2154,7 @@ namespace Interface
++objectsReplaced;
}
- if ( replaceKey( _mapFormat.spellObjectMetadata, object.id, newObjectUID ) ) {
+ if ( replaceKey( _mapFormat.selectionObjectMetadata, object.id, newObjectUID ) ) {
++objectsReplaced;
}
diff --git a/src/fheroes2/editor/editor_secondary_skill_selection.cpp b/src/fheroes2/editor/editor_secondary_skill_selection.cpp
new file mode 100644
index 00000000000..f0318de43b9
--- /dev/null
+++ b/src/fheroes2/editor/editor_secondary_skill_selection.cpp
@@ -0,0 +1,330 @@
+/***************************************************************************
+ * fheroes2: https://github.com/ihhub/fheroes2 *
+ * Copyright (C) 2024 *
+ * *
+ * 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 "editor_secondary_skill_selection.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "agg_image.h"
+#include "cursor.h"
+#include "dialog.h"
+#include "game_hotkeys.h"
+#include "game_static.h"
+#include "heroes.h"
+#include "icn.h"
+#include "image.h"
+#include "localevent.h"
+#include "math_base.h"
+#include "math_tools.h"
+#include "pal.h"
+#include "screen.h"
+#include "settings.h"
+#include "skill.h"
+#include "translations.h"
+#include "ui_button.h"
+#include "ui_dialog.h"
+#include "ui_text.h"
+#include "ui_window.h"
+
+namespace
+{
+ const int32_t skillRowOffsetY{ 90 };
+ const int32_t skillItemWidth{ 90 };
+
+ // Up to 5 skills can be displayed in a row.
+ // Up to 5 rows can be displayed.
+ // So far the number of skills of any level is much more than 25.
+ class SecondarySkillContainerUI final
+ {
+ public:
+ SecondarySkillContainerUI( fheroes2::Point offset, std::vector> & skills )
+ : _skills( skills )
+ {
+ assert( !skills.empty() && skills.size() < 25 );
+
+ // Figure out how many rows and columns we want to display.
+ for ( size_t i = 1; i < 5; ++i ) {
+ if ( i * i >= skills.size() ) {
+ _skillsPerRow = i;
+ break;
+ }
+ }
+
+ offset.x += ( fheroes2::Display::DEFAULT_WIDTH - static_cast( _skillsPerRow ) * skillItemWidth ) / 2;
+ offset.y += ( fheroes2::Display::DEFAULT_HEIGHT - 50 - skillRowOffsetY * static_cast( _skills.size() / _skillsPerRow ) ) / 2;
+
+ // Calculate all areas where we are going to render skills.
+ _skillRoi.reserve( _skills.size() );
+
+ const fheroes2::Sprite & frameImage = fheroes2::AGG::GetICN( ICN::SECSKILL, 15 );
+
+ const int32_t lastRowColumns = static_cast( _skills.size() % _skillsPerRow );
+ const int32_t lastRowOffsetX = ( lastRowColumns > 0 ) ? ( static_cast( _skillsPerRow ) - lastRowColumns ) * skillItemWidth / 2 : 0;
+
+ for ( size_t i = 0; i < _skills.size(); ++i ) {
+ const int32_t rowId = static_cast( i / _skillsPerRow );
+ const int32_t columnId = static_cast( i % _skillsPerRow );
+
+ if ( rowId == static_cast( _skills.size() / _skillsPerRow ) ) {
+ // This is the last row.
+ _skillRoi.emplace_back( offset.x + columnId * skillItemWidth + lastRowOffsetX, offset.y + rowId * skillRowOffsetY, frameImage.width(),
+ frameImage.height() );
+ }
+ else {
+ _skillRoi.emplace_back( offset.x + columnId * skillItemWidth, offset.y + rowId * skillRowOffsetY, frameImage.width(), frameImage.height() );
+ }
+ }
+ }
+
+ void draw( fheroes2::Image & output )
+ {
+ const fheroes2::Sprite & frameImage = fheroes2::AGG::GetICN( ICN::SECSKILL, 15 );
+
+ fheroes2::Sprite inactiveFrameImage( frameImage );
+ fheroes2::ApplyPalette( inactiveFrameImage, PAL::GetPalette( PAL::PaletteType::GRAY ) );
+
+ for ( size_t i = 0; i < _skills.size(); ++i ) {
+ const Skill::Secondary & skill = _skills[i].first;
+
+ const fheroes2::Sprite & skillImage = fheroes2::AGG::GetICN( ICN::SECSKILL, skill.GetIndexSprite1() );
+ const fheroes2::Point skillImagePos( _skillRoi[i].x + 3, _skillRoi[i].y + 3 );
+
+ fheroes2::Copy( skillImage, 0, 0, output, skillImagePos.x, skillImagePos.y, skillImage.width(), skillImage.height() );
+
+ if ( _skills[i].second ) {
+ fheroes2::Blit( frameImage, output, _skillRoi[i].x, _skillRoi[i].y );
+ }
+ else {
+ // The skill is being inactive.
+ fheroes2::Blit( inactiveFrameImage, output, _skillRoi[i].x, _skillRoi[i].y );
+ fheroes2::ApplyPalette( output, skillImagePos.x, skillImagePos.y, output, _skillRoi[i].x + 3, _skillRoi[i].y + 3, skillImage.width(),
+ skillImage.height(), PAL::GetPalette( PAL::PaletteType::GRAY ) );
+ }
+
+ fheroes2::Text text{ Skill::Secondary::String( skill.Skill() ), fheroes2::FontType::smallWhite() };
+ text.drawInRoi( skillImagePos.x + ( skillImage.width() - text.width() ) / 2, _skillRoi[i].y + 7, output, _skillRoi[i] );
+ text.set( Skill::Level::String( skill.Level() ), fheroes2::FontType::smallWhite() );
+ text.drawInRoi( skillImagePos.x + ( skillImage.width() - text.width() ) / 2, _skillRoi[i].y + skillImage.height() - 10, output, _skillRoi[i] );
+ }
+ }
+
+ const fheroes2::Rect & redrawChangedSkill( fheroes2::Image & output )
+ {
+ assert( _skillToRedraw >= 0 && _skillToRedraw < static_cast( _skillRoi.size() ) );
+
+ const fheroes2::Rect & roi = _skillRoi[_skillToRedraw];
+
+ if ( _skills[_skillToRedraw].second ) {
+ const Skill::Secondary & skill = _skills[_skillToRedraw].first;
+
+ const fheroes2::Sprite & frameImage = fheroes2::AGG::GetICN( ICN::SECSKILL, 15 );
+ const fheroes2::Sprite & skillImage = fheroes2::AGG::GetICN( ICN::SECSKILL, skill.GetIndexSprite1() );
+ const int32_t skillImageOffsetX = roi.x + 3;
+
+ fheroes2::Blit( frameImage, output, roi.x, roi.y );
+ fheroes2::Copy( skillImage, 0, 0, output, skillImageOffsetX, roi.y + 3, skillImage.width(), skillImage.height() );
+
+ fheroes2::Text text{ Skill::Secondary::String( skill.Skill() ), fheroes2::FontType::smallWhite() };
+ text.drawInRoi( skillImageOffsetX + ( skillImage.width() - text.width() ) / 2, roi.y + 7, output, roi );
+ text.set( Skill::Level::String( skill.Level() ), fheroes2::FontType::smallWhite() );
+ text.drawInRoi( skillImageOffsetX + ( skillImage.width() - text.width() ) / 2, roi.y + skillImage.height() - 10, output, roi );
+ }
+ else {
+ // The skill is being inactive. Just make it grayscale.
+ fheroes2::ApplyPalette( output, roi.x, roi.y, output, roi.x, roi.y, roi.width, roi.height, PAL::GetPalette( PAL::PaletteType::GRAY ) );
+ }
+
+ return roi;
+ }
+
+ bool processEvents( LocalEvent & eventProcessor )
+ {
+ const int32_t skillIndex = GetRectIndex( _skillRoi, eventProcessor.getMouseCursorPos() );
+ if ( skillIndex < 0 ) {
+ return false;
+ }
+
+ assert( static_cast( skillIndex ) < _skillRoi.size() );
+
+ const fheroes2::Rect skillRoi = _skillRoi[skillIndex];
+
+ if ( eventProcessor.MouseClickLeft( skillRoi ) ) {
+ _skills[skillIndex].second = !_skills[skillIndex].second;
+
+ _skillToRedraw = skillIndex;
+ return true;
+ }
+
+ if ( eventProcessor.isMouseRightButtonPressedInArea( skillRoi ) ) {
+ const Heroes fakeHero;
+ fheroes2::SecondarySkillDialogElement( _skills[skillIndex].first, fakeHero ).showPopup( Dialog::ZERO );
+ }
+
+ return false;
+ }
+
+ private:
+ std::vector> & _skills;
+
+ std::vector _skillRoi;
+
+ size_t _skillsPerRow{ 0 };
+ int32_t _skillToRedraw{ 0 };
+ };
+}
+
+namespace Editor
+{
+ bool openSecondarySkillSelectionWindow( std::string title, const int skillLevel, std::vector & selectedSkills )
+ {
+ if ( skillLevel < 1 || skillLevel > 3 ) {
+ // What are you trying to achieve?!
+ assert( 0 );
+ return false;
+ }
+
+ const std::vector & existingSkills = GameStatic::getSecondarySkillsForWitchsHut();
+
+ // Create a container of active and disabled skills.
+ std::vector> skills;
+ skills.reserve( existingSkills.size() );
+
+ bool isAnySkillEnabled = false;
+
+ for ( const int skill : existingSkills ) {
+ const bool isSelected = ( std::find( selectedSkills.begin(), selectedSkills.end(), skill ) != selectedSkills.end() );
+
+ skills.emplace_back( Skill::Secondary( skill, skillLevel ), isSelected );
+
+ if ( isSelected ) {
+ isAnySkillEnabled = true;
+ }
+ }
+
+ // If no skills are selected, select all of them.
+ if ( !isAnySkillEnabled ) {
+ for ( auto & [skill, isSelected] : skills ) {
+ isSelected = true;
+ }
+ }
+
+ const CursorRestorer cursorRestorer( true, Cursor::POINTER );
+
+ fheroes2::Display & display = fheroes2::Display::instance();
+
+ const bool isDefaultScreenSize = display.isDefaultSize();
+
+ fheroes2::StandardWindow background( fheroes2::Display::DEFAULT_WIDTH, fheroes2::Display::DEFAULT_HEIGHT, !isDefaultScreenSize );
+ const fheroes2::Rect activeArea( background.activeArea() );
+
+ const bool isEvilInterface = Settings::Get().isEvilInterfaceEnabled();
+
+ if ( isDefaultScreenSize ) {
+ const fheroes2::Sprite & backgroundImage = fheroes2::AGG::GetICN( isEvilInterface ? ICN::STONEBAK_EVIL : ICN::STONEBAK, 0 );
+ fheroes2::Copy( backgroundImage, 0, 0, display, activeArea );
+ }
+
+ const fheroes2::Text text( std::move( title ), fheroes2::FontType::normalYellow() );
+ text.draw( activeArea.x + ( activeArea.width - text.width() ) / 2, activeArea.y + 10, display );
+
+ // Buttons.
+ fheroes2::Button buttonOk;
+ fheroes2::Button buttonCancel;
+
+ background.renderOkayCancelButtons( buttonOk, buttonCancel, isEvilInterface );
+
+ SecondarySkillContainerUI skillContainer( activeArea.getPosition(), skills );
+
+ skillContainer.draw( display );
+
+ display.render( background.totalArea() );
+
+ LocalEvent & le = LocalEvent::Get();
+ while ( le.HandleEvents() ) {
+ if ( buttonOk.isEnabled() ) {
+ buttonOk.drawOnState( le.isMouseLeftButtonPressedInArea( buttonOk.area() ) );
+ }
+
+ buttonCancel.drawOnState( le.isMouseLeftButtonPressedInArea( buttonCancel.area() ) );
+
+ if ( Game::HotKeyPressEvent( Game::HotKeyEvent::DEFAULT_CANCEL ) || le.MouseClickLeft( buttonCancel.area() ) ) {
+ return false;
+ }
+
+ if ( buttonOk.isEnabled() && ( Game::HotKeyPressEvent( Game::HotKeyEvent::DEFAULT_OKAY ) || le.MouseClickLeft( buttonOk.area() ) ) ) {
+ break;
+ }
+
+ if ( le.isMouseRightButtonPressedInArea( buttonCancel.area() ) ) {
+ fheroes2::showStandardTextMessage( _( "Cancel" ), _( "Exit this menu without doing anything." ), Dialog::ZERO );
+ }
+ else if ( le.isMouseRightButtonPressedInArea( buttonOk.area() ) ) {
+ fheroes2::showStandardTextMessage( _( "Okay" ), _( "Click to accept the changes made." ), Dialog::ZERO );
+ }
+
+ if ( skillContainer.processEvents( le ) ) {
+ const fheroes2::Rect & roi = skillContainer.redrawChangedSkill( display );
+
+ // Check if all skills are being disabled. If they are disable the OKAY button.
+ bool areAllSkillsDisabled = true;
+ for ( const auto & [skill, isSelected] : skills ) {
+ if ( isSelected ) {
+ areAllSkillsDisabled = false;
+ break;
+ }
+ }
+
+ if ( areAllSkillsDisabled && buttonOk.isEnabled() ) {
+ buttonOk.disable();
+ buttonOk.draw();
+
+ display.updateNextRenderRoi( buttonOk.area() );
+ }
+ else if ( buttonOk.isDisabled() ) {
+ buttonOk.enable();
+ buttonOk.draw();
+
+ display.updateNextRenderRoi( buttonOk.area() );
+ }
+
+ display.render( roi );
+ }
+ }
+
+ selectedSkills.clear();
+
+ for ( const auto & [skill, isSelected] : skills ) {
+ if ( isSelected ) {
+ selectedSkills.emplace_back( skill.first );
+ }
+ }
+
+ // If all skills are selected, remove all skills from the selection since an empty container means the use of the default behavior of the game.
+ if ( selectedSkills.size() == skills.size() ) {
+ selectedSkills = {};
+ }
+
+ return true;
+ }
+}
diff --git a/src/fheroes2/editor/editor_secondary_skill_selection.h b/src/fheroes2/editor/editor_secondary_skill_selection.h
new file mode 100644
index 00000000000..6c97c6ff98d
--- /dev/null
+++ b/src/fheroes2/editor/editor_secondary_skill_selection.h
@@ -0,0 +1,30 @@
+/***************************************************************************
+ * fheroes2: https://github.com/ihhub/fheroes2 *
+ * Copyright (C) 2024 *
+ * *
+ * 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
+#include
+
+namespace Editor
+{
+ bool openSecondarySkillSelectionWindow( std::string title, const int skillLevel, std::vector & selectedSkills );
+}
diff --git a/src/fheroes2/editor/editor_spell_selection.cpp b/src/fheroes2/editor/editor_spell_selection.cpp
index a934b730822..82f2b5a5098 100644
--- a/src/fheroes2/editor/editor_spell_selection.cpp
+++ b/src/fheroes2/editor/editor_spell_selection.cpp
@@ -138,14 +138,17 @@ namespace
return false;
}
- if ( eventProcessor.MouseClickLeft() ) {
- assert( static_cast( spellIndex ) < _spellRoi.size() );
+ assert( static_cast( spellIndex ) < _spellRoi.size() );
+ const fheroes2::Rect spellRoi = _spellRoi[spellIndex];
+
+ if ( eventProcessor.MouseClickLeft( spellRoi ) ) {
_spells[spellIndex].second = !_spells[spellIndex].second;
+
return true;
}
- if ( eventProcessor.isMouseRightButtonPressed() ) {
+ if ( eventProcessor.isMouseRightButtonPressedInArea( spellRoi ) ) {
fheroes2::SpellDialogElement( _spells[spellIndex].first, nullptr ).showPopup( Dialog::ZERO );
}
@@ -180,7 +183,7 @@ namespace Editor
bool isAnySpellEnabled = false;
- for ( const int & spell : availableSpells ) {
+ for ( const int spell : availableSpells ) {
const bool isSelected = ( std::find( selectedSpells.begin(), selectedSpells.end(), spell ) != selectedSpells.end() );
spells.emplace_back( spell, isSelected );
@@ -259,15 +262,15 @@ namespace Editor
spellContainer.draw( display );
// Check if all spells are being disabled. If they are disable the OKAY button.
- bool areAllSpelledDisabled = true;
+ bool areAllSpellsDisabled = true;
for ( const auto & [spell, isSelected] : spells ) {
if ( isSelected ) {
- areAllSpelledDisabled = false;
+ areAllSpellsDisabled = false;
break;
}
}
- if ( areAllSpelledDisabled ) {
+ if ( areAllSpellsDisabled ) {
buttonOk.disable();
buttonOk.draw();
}
diff --git a/src/fheroes2/game/game_static.cpp b/src/fheroes2/game/game_static.cpp
index bbc93bda260..b69bab8dd15 100644
--- a/src/fheroes2/game/game_static.cpp
+++ b/src/fheroes2/game/game_static.cpp
@@ -114,23 +114,6 @@ namespace
{ "estates", { 100, 250, 500 } },
{ nullptr, { 0, 0, 0 } },
} };
-
- const Skill::SecondarySkillValues secondarySkillValuesForWitchsHut = {
- 1, // archery
- 1, // ballistics
- 1, // diplomacy
- 1, // eagleeye
- 1, // estates
- 0, // leadership
- 1, // logistics
- 1, // luck
- 1, // mysticism
- 1, // navigation
- 0, // necromancy
- 1, // pathfinding
- 1, // scouting
- 1 // wisdom
- };
}
uint32_t GameStatic::GetLostOnWhirlpoolPercent()
@@ -277,9 +260,14 @@ const Skill::SecondarySkillValuesPerLevel * GameStatic::GetSecondarySkillValuesP
return nullptr;
}
-const Skill::SecondarySkillValues * GameStatic::GetSecondarySkillValuesForWitchsHut()
+const std::vector & GameStatic::getSecondarySkillsForWitchsHut()
{
- return &secondarySkillValuesForWitchsHut;
+ // Every skill except Leadership and Necromancy.
+ static const std::vector skills{ Skill::Secondary::PATHFINDING, Skill::Secondary::ARCHERY, Skill::Secondary::LOGISTICS, Skill::Secondary::SCOUTING,
+ Skill::Secondary::DIPLOMACY, Skill::Secondary::NAVIGATION, Skill::Secondary::WISDOM, Skill::Secondary::MYSTICISM,
+ Skill::Secondary::LUCK, Skill::Secondary::BALLISTICS, Skill::Secondary::EAGLE_EYE, Skill::Secondary::ESTATES };
+
+ return skills;
}
int GameStatic::GetBattleMoatReduceDefense()
diff --git a/src/fheroes2/game/game_static.h b/src/fheroes2/game/game_static.h
index ddea682eb19..a7d4a2c175a 100644
--- a/src/fheroes2/game/game_static.h
+++ b/src/fheroes2/game/game_static.h
@@ -25,6 +25,7 @@
#define H2GAMESTATIC_H
#include
+#include
namespace MP2
{
@@ -35,7 +36,6 @@ namespace Skill
{
struct FactionProperties;
struct SecondarySkillValuesPerLevel;
- struct SecondarySkillValues;
}
class Heroes;
@@ -71,7 +71,8 @@ namespace GameStatic
const Skill::FactionProperties * GetFactionProperties( const int race );
const Skill::SecondarySkillValuesPerLevel * GetSecondarySkillValuesPerLevel( const int skill );
- const Skill::SecondarySkillValues * GetSecondarySkillValuesForWitchsHut();
+
+ const std::vector & getSecondarySkillsForWitchsHut();
uint32_t getMovementPointBonus( const MP2::MapObjectType objectType );
diff --git a/src/fheroes2/gui/ui_campaign.cpp b/src/fheroes2/gui/ui_campaign.cpp
index d96d28c9b2f..e4852fd7234 100644
--- a/src/fheroes2/gui/ui_campaign.cpp
+++ b/src/fheroes2/gui/ui_campaign.cpp
@@ -122,7 +122,7 @@ namespace fheroes2
break;
}
case Campaign::ScenarioBonusData::SKILL_SECONDARY: {
- Heroes fakeHero;
+ const Heroes fakeHero;
Skill::Secondary skill( bonusData._subType, bonusData._amount );
const SecondarySkillDialogElement secondarySkillUI( skill, fakeHero );
const TextDialogElement skillDescriptionUI( std::make_shared( skill.GetDescription( fakeHero ), FontType::normalWhite() ) );
diff --git a/src/fheroes2/heroes/skill.cpp b/src/fheroes2/heroes/skill.cpp
index d2c64a9f516..ae7a7d094d6 100644
--- a/src/fheroes2/heroes/skill.cpp
+++ b/src/fheroes2/heroes/skill.cpp
@@ -387,62 +387,6 @@ bool Skill::Secondary::isValid() const
return Skill() != UNKNOWN && Level() != Level::NONE;
}
-int Skill::Secondary::RandForWitchsHut()
-{
- const Skill::SecondarySkillValues * ptr = GameStatic::GetSecondarySkillValuesForWitchsHut();
- if ( ptr == nullptr ) {
- return UNKNOWN;
- }
-
- std::vector v;
- v.reserve( 14 );
-
- if ( ptr->archery ) {
- v.push_back( ARCHERY );
- }
- if ( ptr->ballistics ) {
- v.push_back( BALLISTICS );
- }
- if ( ptr->diplomacy ) {
- v.push_back( DIPLOMACY );
- }
- if ( ptr->eagleeye ) {
- v.push_back( EAGLE_EYE );
- }
- if ( ptr->estates ) {
- v.push_back( ESTATES );
- }
- if ( ptr->leadership ) {
- v.push_back( LEADERSHIP );
- }
- if ( ptr->logistics ) {
- v.push_back( LOGISTICS );
- }
- if ( ptr->luck ) {
- v.push_back( LUCK );
- }
- if ( ptr->mysticism ) {
- v.push_back( MYSTICISM );
- }
- if ( ptr->navigation ) {
- v.push_back( NAVIGATION );
- }
- if ( ptr->necromancy ) {
- v.push_back( NECROMANCY );
- }
- if ( ptr->pathfinding ) {
- v.push_back( PATHFINDING );
- }
- if ( ptr->scouting ) {
- v.push_back( SCOUTING );
- }
- if ( ptr->wisdom ) {
- v.push_back( WISDOM );
- }
-
- return v.empty() ? UNKNOWN : Rand::Get( v );
-}
-
int Skill::Secondary::GetIndexSprite1() const
{
return ( Skill() > UNKNOWN && Skill() <= ESTATES ) ? Skill() : 0;
diff --git a/src/fheroes2/heroes/skill.h b/src/fheroes2/heroes/skill.h
index 0cec02d317b..cdf2fec79ab 100644
--- a/src/fheroes2/heroes/skill.h
+++ b/src/fheroes2/heroes/skill.h
@@ -122,7 +122,6 @@ namespace Skill
// Returns the sprite index from MINISS
int GetIndexSprite2() const;
- static int RandForWitchsHut();
static const char * String( int );
};
diff --git a/src/fheroes2/maps/map_format_info.cpp b/src/fheroes2/maps/map_format_info.cpp
index 61e964af21f..e6528c546c7 100644
--- a/src/fheroes2/maps/map_format_info.cpp
+++ b/src/fheroes2/maps/map_format_info.cpp
@@ -57,8 +57,8 @@ namespace Maps::Map_Format
OStreamBase & operator<<( OStreamBase & stream, const AdventureMapEventMetadata & metadata );
IStreamBase & operator>>( IStreamBase & stream, AdventureMapEventMetadata & metadata );
- OStreamBase & operator<<( OStreamBase & stream, const SpellObjectMetadata & metadata );
- IStreamBase & operator>>( IStreamBase & stream, SpellObjectMetadata & metadata );
+ OStreamBase & operator<<( OStreamBase & stream, const SelectionObjectMetadata & metadata );
+ IStreamBase & operator>>( IStreamBase & stream, SelectionObjectMetadata & metadata );
}
namespace
@@ -238,7 +238,7 @@ namespace
compressed.setBigendian( true );
compressed << map.additionalInfo << map.tiles << map.dailyEvents << map.rumors << map.standardMetadata << map.castleMetadata << map.heroMetadata
- << map.sphinxMetadata << map.signMetadata << map.adventureMapEventMetadata << map.spellObjectMetadata;
+ << map.sphinxMetadata << map.signMetadata << map.adventureMapEventMetadata << map.selectionObjectMetadata;
const std::vector temp = Compression::zipData( compressed.data(), compressed.size() );
@@ -287,7 +287,7 @@ namespace
}
decompressed >> map.dailyEvents >> map.rumors >> map.standardMetadata >> map.castleMetadata >> map.heroMetadata >> map.sphinxMetadata >> map.signMetadata
- >> map.adventureMapEventMetadata >> map.spellObjectMetadata;
+ >> map.adventureMapEventMetadata >> map.selectionObjectMetadata;
convertFromV2ToV3( map );
convertFromV3ToV4( map );
@@ -405,14 +405,14 @@ namespace Maps::Map_Format
>> metadata.experience >> metadata.secondarySkill >> metadata.secondarySkillLevel >> metadata.monsterType >> metadata.monsterCount;
}
- OStreamBase & operator<<( OStreamBase & stream, const SpellObjectMetadata & metadata )
+ OStreamBase & operator<<( OStreamBase & stream, const SelectionObjectMetadata & metadata )
{
- return stream << metadata.allowedSpells;
+ return stream << metadata.selectedItems;
}
- IStreamBase & operator>>( IStreamBase & stream, SpellObjectMetadata & metadata )
+ IStreamBase & operator>>( IStreamBase & stream, SelectionObjectMetadata & metadata )
{
- return stream >> metadata.allowedSpells;
+ return stream >> metadata.selectedItems;
}
bool loadBaseMap( const std::string & path, BaseMapFormat & map )
diff --git a/src/fheroes2/maps/map_format_info.h b/src/fheroes2/maps/map_format_info.h
index e0c6724c19c..a546a7d5364 100644
--- a/src/fheroes2/maps/map_format_info.h
+++ b/src/fheroes2/maps/map_format_info.h
@@ -248,9 +248,9 @@ namespace Maps::Map_Format
}
};
- struct SpellObjectMetadata
+ struct SelectionObjectMetadata
{
- std::vector allowedSpells;
+ std::vector selectedItems;
};
struct DailyEvent
@@ -329,7 +329,7 @@ namespace Maps::Map_Format
std::map adventureMapEventMetadata;
- std::map spellObjectMetadata;
+ std::map selectionObjectMetadata;
};
bool loadBaseMap( const std::string & path, BaseMapFormat & map );
diff --git a/src/fheroes2/maps/maps_tiles_helper.cpp b/src/fheroes2/maps/maps_tiles_helper.cpp
index 3edd81e3119..7083584bd92 100644
--- a/src/fheroes2/maps/maps_tiles_helper.cpp
+++ b/src/fheroes2/maps/maps_tiles_helper.cpp
@@ -39,6 +39,7 @@
#include "castle.h"
#include "color.h"
#include "direction.h"
+#include "game_static.h"
#include "ground.h"
#include "logging.h"
#include "map_object_info.h"
@@ -1955,7 +1956,13 @@ namespace Maps
case MP2::OBJ_WITCHS_HUT:
assert( isFirstLoad );
- tile.metadata()[0] = Skill::Secondary::RandForWitchsHut();
+ static_assert( Skill::Secondary::UNKNOWN == 0, "You are breaking the logic by changing the Skill::Secondary::UNKNOWN value!" );
+ if ( tile.metadata()[0] != Skill::Secondary::UNKNOWN ) {
+ // The skill has been set externally.
+ break;
+ }
+
+ tile.metadata()[0] = Rand::Get( GameStatic::getSecondarySkillsForWitchsHut() );
break;
case MP2::OBJ_SHRINE_FIRST_CIRCLE:
diff --git a/src/fheroes2/world/world_loadmap.cpp b/src/fheroes2/world/world_loadmap.cpp
index 985896d7ec7..4158b6bfc54 100644
--- a/src/fheroes2/world/world_loadmap.cpp
+++ b/src/fheroes2/world/world_loadmap.cpp
@@ -42,6 +42,7 @@
#include "castle.h"
#include "color.h"
#include "game_over.h"
+#include "game_static.h"
#include "heroes.h"
#include "kingdom.h"
#include "logging.h"
@@ -722,18 +723,18 @@ bool World::loadResurrectionMap( const std::string & filename )
std::set sphinxMetadataUIDs;
std::set signMetadataUIDs;
std::set adventureMapEventMetadataUIDs;
- std::set spellObjectMetadataUIDs;
+ std::set selectionObjectMetadataUIDs;
#endif
- const auto areSpellsValid = []( const Maps::Map_Format::SpellObjectMetadata & metadata, const int spellLevel ) {
- if ( metadata.allowedSpells.empty() ) {
+ const auto areSpellsValid = []( const Maps::Map_Format::SelectionObjectMetadata & metadata, const int spellLevel ) {
+ if ( metadata.selectedItems.empty() ) {
// No spells are being set which means that we fall to the default behavior.
return false;
}
const std::vector & spells = Spell::getAllSpellIdsSuitableForSpellBook( spellLevel );
- return std::all_of( metadata.allowedSpells.begin(), metadata.allowedSpells.end(),
+ return std::all_of( metadata.selectedItems.begin(), metadata.selectedItems.end(),
[&spells]( const int32_t spellId ) { return std::find( spells.begin(), spells.end(), spellId ) != spells.end(); } );
};
@@ -925,16 +926,16 @@ bool World::loadResurrectionMap( const std::string & filename )
break;
}
case MP2::OBJ_PYRAMID: {
- if ( map.spellObjectMetadata.find( object.id ) == map.spellObjectMetadata.end() ) {
+ if ( map.selectionObjectMetadata.find( object.id ) == map.selectionObjectMetadata.end() ) {
break;
}
#if defined( WITH_DEBUG )
- spellObjectMetadataUIDs.emplace( object.id );
+ selectionObjectMetadataUIDs.emplace( object.id );
#endif
- const auto & metadata = map.spellObjectMetadata[object.id];
+ const auto & metadata = map.selectionObjectMetadata[object.id];
if ( areSpellsValid( metadata, 5 ) ) {
- vec_tiles[static_cast( tileId )].metadata()[0] = Rand::Get( metadata.allowedSpells );
+ vec_tiles[static_cast( tileId )].metadata()[0] = Rand::Get( metadata.selectedItems );
}
break;
@@ -996,6 +997,33 @@ bool World::loadResurrectionMap( const std::string & filename )
break;
}
+ case MP2::OBJ_WITCHS_HUT: {
+ if ( map.selectionObjectMetadata.find( object.id ) == map.selectionObjectMetadata.end() ) {
+ break;
+ }
+#if defined( WITH_DEBUG )
+ selectionObjectMetadataUIDs.emplace( object.id );
+#endif
+
+ const auto areSkillsValid = []( const Maps::Map_Format::SelectionObjectMetadata & metadata ) {
+ if ( metadata.selectedItems.empty() ) {
+ // No skills are being set which means that we fall to the default behavior.
+ return false;
+ }
+
+ const std::vector & skills = GameStatic::getSecondarySkillsForWitchsHut();
+
+ return std::all_of( metadata.selectedItems.begin(), metadata.selectedItems.end(),
+ [&skills]( const int32_t skillId ) { return std::find( skills.begin(), skills.end(), skillId ) != skills.end(); } );
+ };
+
+ const auto & metadata = map.selectionObjectMetadata[object.id];
+ if ( areSkillsValid( metadata ) ) {
+ vec_tiles[static_cast( tileId )].metadata()[0] = Rand::Get( metadata.selectedItems );
+ }
+
+ break;
+ }
default:
// Other objects do not have metadata as of now.
break;
@@ -1070,13 +1098,13 @@ bool World::loadResurrectionMap( const std::string & filename )
case MP2::OBJ_SHRINE_FIRST_CIRCLE:
case MP2::OBJ_SHRINE_SECOND_CIRCLE:
case MP2::OBJ_SHRINE_THIRD_CIRCLE: {
- if ( map.spellObjectMetadata.find( object.id ) == map.spellObjectMetadata.end() ) {
+ if ( map.selectionObjectMetadata.find( object.id ) == map.selectionObjectMetadata.end() ) {
break;
}
#if defined( WITH_DEBUG )
- spellObjectMetadataUIDs.emplace( object.id );
+ selectionObjectMetadataUIDs.emplace( object.id );
#endif
- const auto & metadata = map.spellObjectMetadata[object.id];
+ const auto & metadata = map.selectionObjectMetadata[object.id];
int spellLevel = 0;
if ( objectType == MP2::OBJ_SHRINE_FIRST_CIRCLE ) {
@@ -1094,7 +1122,7 @@ bool World::loadResurrectionMap( const std::string & filename )
}
if ( areSpellsValid( metadata, spellLevel ) ) {
- vec_tiles[static_cast( tileId )].metadata()[0] = Rand::Get( metadata.allowedSpells );
+ vec_tiles[static_cast( tileId )].metadata()[0] = Rand::Get( metadata.selectedItems );
}
break;
@@ -1113,7 +1141,7 @@ bool World::loadResurrectionMap( const std::string & filename )
assert( sphinxMetadataUIDs.size() == map.sphinxMetadata.size() );
assert( signMetadataUIDs.size() == map.signMetadata.size() );
assert( adventureMapEventMetadataUIDs.size() == map.adventureMapEventMetadata.size() );
- assert( spellObjectMetadataUIDs.size() == map.spellObjectMetadata.size() );
+ assert( selectionObjectMetadataUIDs.size() == map.selectionObjectMetadata.size() );
for ( const uint32_t uid : standardMetadataUIDs ) {
assert( map.standardMetadata.find( uid ) != map.standardMetadata.end() );
@@ -1139,8 +1167,8 @@ bool World::loadResurrectionMap( const std::string & filename )
assert( map.adventureMapEventMetadata.find( uid ) != map.adventureMapEventMetadata.end() );
}
- for ( const uint32_t uid : spellObjectMetadataUIDs ) {
- assert( map.spellObjectMetadata.find( uid ) != map.spellObjectMetadata.end() );
+ for ( const uint32_t uid : selectionObjectMetadataUIDs ) {
+ assert( map.selectionObjectMetadata.find( uid ) != map.selectionObjectMetadata.end() );
}
#endif