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