Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to set spells in Shrine in the Editor #9347

Merged
merged 12 commits into from
Dec 18, 2024
2 changes: 2 additions & 0 deletions VisualStudio/fheroes2/sources.props
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
<ClCompile Include="src\fheroes2\editor\editor_options.cpp" />
<ClCompile Include="src\fheroes2\editor\editor_rumor_window.cpp" />
<ClCompile Include="src\fheroes2\editor\editor_save_map_window.cpp" />
<ClCompile Include="src\fheroes2\editor\editor_spell_selection.cpp" />
<ClCompile Include="src\fheroes2\editor\editor_sphinx_window.cpp" />
<ClCompile Include="src\fheroes2\editor\editor_ui_helper.cpp" />
<ClCompile Include="src\fheroes2\editor\history_manager.cpp" />
Expand Down Expand Up @@ -327,6 +328,7 @@
<ClInclude Include="src\fheroes2\editor\editor_options.h" />
<ClInclude Include="src\fheroes2\editor\editor_rumor_window.h" />
<ClInclude Include="src\fheroes2\editor\editor_save_map_window.h" />
<ClInclude Include="src\fheroes2\editor\editor_spell_selection.h" />
<ClInclude Include="src\fheroes2\editor\editor_sphinx_window.h" />
<ClInclude Include="src\fheroes2\editor\editor_ui_helper.h" />
<ClInclude Include="src\fheroes2\editor\history_manager.h" />
Expand Down
51 changes: 51 additions & 0 deletions src/fheroes2/editor/editor_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include "editor_map_specs_window.h"
#include "editor_object_popup_window.h"
#include "editor_save_map_window.h"
#include "editor_spell_selection.h"
#include "editor_sphinx_window.h"
#include "game.h"
#include "game_delays.h"
Expand Down Expand Up @@ -397,6 +398,25 @@ namespace

needRedraw = true;
}
else if ( objectIter->group == Maps::ObjectGroup::ADVENTURE_POWER_UPS ) {
const auto & objects = Maps::getObjectsByGroup( objectIter->group );

assert( objectIter->index < objects.size() );
const auto objectType = objects[objectIter->index].objectType;
switch ( objectType ) {
case MP2::OBJ_SHRINE_FIRST_CIRCLE:
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.shrineMetadata.erase( objectIter->id );
break;
default:
break;
}

objectIter = mapTile.objects.erase( objectIter );
needRedraw = true;
}
else {
objectIter = mapTile.objects.erase( objectIter );
needRedraw = true;
Expand Down Expand Up @@ -1343,6 +1363,37 @@ namespace Interface
action.commit();
}
}
else if ( objectType == MP2::OBJ_SHRINE_FIRST_CIRCLE || objectType == MP2::OBJ_SHRINE_SECOND_CIRCLE || objectType == MP2::OBJ_SHRINE_THIRD_CIRCLE ) {
auto shrineMetadata = _mapFormat.shrineMetadata.find( object.id );
if ( shrineMetadata == _mapFormat.shrineMetadata.end() ) {
_mapFormat.shrineMetadata[object.id] = {};
}

auto & originalMetadata = _mapFormat.shrineMetadata[object.id];
auto newMetadata = originalMetadata;

int spellLevel = 0;
if ( objectType == MP2::OBJ_SHRINE_FIRST_CIRCLE ) {
spellLevel = 1;
}
else if ( objectType == MP2::OBJ_SHRINE_SECOND_CIRCLE ) {
spellLevel = 2;
}
else if ( objectType == MP2::OBJ_SHRINE_THIRD_CIRCLE ) {
spellLevel = 3;
}
else {
assert( 0 );
spellLevel = 1;
}

if ( Editor::openSpellSelectionWindow( MP2::StringObject( objectType ), spellLevel, newMetadata.allowedSpells )
&& originalMetadata.allowedSpells != newMetadata.allowedSpells ) {
fheroes2::ActionCreator action( _historyManager, _mapFormat );
originalMetadata = std::move( newMetadata );
action.commit();
}
}
else {
std::string msg = _( "%{object} has no properties to change." );
StringReplace( msg, "%{object}", MP2::StringObject( objectType ) );
Expand Down
298 changes: 298 additions & 0 deletions src/fheroes2/editor/editor_spell_selection.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
/***************************************************************************
* 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_spell_selection.h"

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <initializer_list>
#include <utility>

#include "agg_image.h"
#include "cursor.h"
#include "dialog.h"
#include "game_hotkeys.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 "spell.h"
#include "translations.h"
#include "ui_button.h"
#include "ui_dialog.h"
#include "ui_text.h"
#include "ui_window.h"

namespace
{
const int32_t spellRowOffsetY{ 90 };
const int32_t spellItemWidth{ 110 };

// Up to 5 spells can be displayed in a row.
// Up to 5 rows can be displayed.
// So far the number of spells of any level is much more than 25.
class SpellContainerUI final
{
public:
SpellContainerUI( fheroes2::Point offset, std::vector<std::pair<Spell, bool>> & spells )
: _spells( spells )
{
assert( !spells.empty() && spells.size() < 25 );

// Figure out how many rows and columns we want to display.
for ( size_t i = 1; i < 5; ++i ) {
if ( i * i >= _spells.size() ) {
_spellsPerRow = i;
break;
}
}

offset.x += ( fheroes2::Display::DEFAULT_WIDTH - static_cast<int32_t>( _spellsPerRow ) * spellItemWidth ) / 2;
offset.y += ( fheroes2::Display::DEFAULT_HEIGHT - 50 - spellRowOffsetY * static_cast<int32_t>( _spells.size() / _spellsPerRow ) ) / 2;

// Calculate all areas where we are going to render spells.
_spellRoi.reserve( _spells.size() );

const fheroes2::Sprite & scrollImage = fheroes2::AGG::GetICN( ICN::TOWNWIND, 0 );

const int32_t lastRowColumns = static_cast<int32_t>( _spells.size() % _spellsPerRow );
const int32_t lastRowOffsetX = ( lastRowColumns > 0 ) ? ( static_cast<int32_t>( _spellsPerRow ) - lastRowColumns ) * spellItemWidth / 2 : 0;

for ( size_t i = 0; i < _spells.size(); ++i ) {
const int32_t rowId = static_cast<int32_t>( i / _spellsPerRow );
const int32_t columnId = static_cast<int32_t>( i % _spellsPerRow );

if ( rowId == static_cast<int32_t>( _spells.size() / _spellsPerRow ) ) {
// This is the last row.
_spellRoi.emplace_back( offset.x + columnId * spellItemWidth + lastRowOffsetX, offset.y + rowId * spellRowOffsetY, scrollImage.width(),
scrollImage.height() );
}
else {
_spellRoi.emplace_back( offset.x + columnId * spellItemWidth, offset.y + rowId * spellRowOffsetY, scrollImage.width(), scrollImage.height() );
}
}
}

void draw( fheroes2::Image & output )
{
const fheroes2::Sprite & scrollImage = fheroes2::AGG::GetICN( ICN::TOWNWIND, 0 );

fheroes2::Sprite inactiveScrollImage( scrollImage );
fheroes2::ApplyPalette( inactiveScrollImage, PAL::GetPalette( PAL::PaletteType::GRAY ) );

for ( size_t i = 0; i < _spells.size(); ++i ) {
if ( !_spells[i].second ) {
// The spell is being inactive.
fheroes2::Blit( inactiveScrollImage, output, _spellRoi[i].x, _spellRoi[i].y );
}
else {
fheroes2::Blit( scrollImage, output, _spellRoi[i].x, _spellRoi[i].y );
}

const fheroes2::Sprite & spellImage = fheroes2::AGG::GetICN( ICN::SPELLS, _spells[i].first.IndexSprite() );

if ( !_spells[i].second ) {
// The spell is being inactive.
fheroes2::Sprite inactiveSpellImage( spellImage );
fheroes2::ApplyPalette( inactiveSpellImage, PAL::GetPalette( PAL::PaletteType::GRAY ) );

fheroes2::Blit( inactiveSpellImage, output, _spellRoi[i].x + 3 + ( _spellRoi[i].width - inactiveSpellImage.width() ) / 2,
_spellRoi[i].y + 31 - inactiveSpellImage.height() / 2 );
}
else {
fheroes2::Blit( spellImage, output, _spellRoi[i].x + 3 + ( _spellRoi[i].width - spellImage.width() ) / 2,
_spellRoi[i].y + 31 - spellImage.height() / 2 );
}

const fheroes2::Text text( _spells[i].first.GetName(), fheroes2::FontType::smallWhite() );
text.draw( _spellRoi[i].x + 18, _spellRoi[i].y + 57, 78, fheroes2::Display::instance() );
}
}

bool processEvents( LocalEvent & eventProcessor )
{
const int32_t spellIndex = GetRectIndex( _spellRoi, eventProcessor.getMouseCursorPos() );
if ( spellIndex < 0 ) {
return false;
}

if ( eventProcessor.MouseClickLeft() ) {
assert( static_cast<size_t>( spellIndex ) < _spellRoi.size() );

_spells[spellIndex].second = !_spells[spellIndex].second;
return true;
}

if ( eventProcessor.isMouseRightButtonPressed() ) {
fheroes2::SpellDialogElement( _spells[spellIndex].first, nullptr ).showPopup( Dialog::ZERO );
}

return false;
}

private:
std::vector<std::pair<Spell, bool>> & _spells;
ihhub marked this conversation as resolved.
Show resolved Hide resolved

std::vector<fheroes2::Rect> _spellRoi;

size_t _spellsPerRow{ 0 };
};
}

namespace Editor
{
bool openSpellSelectionWindow( std::string title, const int spellLevel, std::vector<int32_t> & selectedSpells )
{
if ( spellLevel < 1 || spellLevel > 5 ) {
// What are you trying to achieve?!
assert( 0 );
return false;
}

const std::vector<int32_t> & availableSpells = Spell::getAllSpellIdsSuitableForSpellBook( spellLevel );
assert( !availableSpells.empty() );

// Create a container of active and disabled spells.
std::vector<std::pair<Spell, bool>> spells;
spells.reserve( availableSpells.size() );

bool isAnySpellEnabled = false;

for ( const int & spell : availableSpells ) {
const bool isSelected = ( std::find( selectedSpells.begin(), selectedSpells.end(), spell ) != selectedSpells.end() );

spells.emplace_back( spell, isSelected );

if ( isSelected ) {
isAnySpellEnabled = true;
}
}

// If no spells are selected, select all of them.
if ( !isAnySpellEnabled ) {
for ( auto & [spell, isSelected] : spells ) {
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 );

fheroes2::ImageRestorer restorer( display, activeArea.x, activeArea.y, activeArea.width, activeArea.height );

SpellContainerUI spellContainer( activeArea.getPosition(), spells );

spellContainer.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 ( spellContainer.processEvents( le ) ) {
restorer.restore();

spellContainer.draw( display );

// Check if all spells are being disabled. If they are disable the OKAY button.
bool areAllSpelledDisabled = true;
for ( const auto & [spell, isSelected] : spells ) {
if ( isSelected ) {
areAllSpelledDisabled = false;
break;
}
}

if ( areAllSpelledDisabled ) {
buttonOk.disable();
buttonOk.draw();
}
else {
buttonOk.enable();
buttonOk.draw();
}

display.render( activeArea );
}
}

selectedSpells.clear();

for ( const auto & [spell, isSelected] : spells ) {
if ( isSelected ) {
selectedSpells.emplace_back( spell.GetID() );
}
}

// If all spells are selected, remove all spells from the selection since an empty container means the use of the default behavior of the game.
if ( selectedSpells.size() == spells.size() ) {
selectedSpells = {};
}

return true;
}
}
Loading
Loading