Skip to content

Commit d9569c8

Browse files
authored
Add ability to set spells in Shrine in the Editor (#9347)
1 parent 2817b51 commit d9569c8

File tree

6 files changed

+461
-0
lines changed

6 files changed

+461
-0
lines changed

VisualStudio/fheroes2/sources.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
<ClCompile Include="src\fheroes2\editor\editor_options.cpp" />
119119
<ClCompile Include="src\fheroes2\editor\editor_rumor_window.cpp" />
120120
<ClCompile Include="src\fheroes2\editor\editor_save_map_window.cpp" />
121+
<ClCompile Include="src\fheroes2\editor\editor_spell_selection.cpp" />
121122
<ClCompile Include="src\fheroes2\editor\editor_sphinx_window.cpp" />
122123
<ClCompile Include="src\fheroes2\editor\editor_ui_helper.cpp" />
123124
<ClCompile Include="src\fheroes2\editor\history_manager.cpp" />
@@ -327,6 +328,7 @@
327328
<ClInclude Include="src\fheroes2\editor\editor_options.h" />
328329
<ClInclude Include="src\fheroes2\editor\editor_rumor_window.h" />
329330
<ClInclude Include="src\fheroes2\editor\editor_save_map_window.h" />
331+
<ClInclude Include="src\fheroes2\editor\editor_spell_selection.h" />
330332
<ClInclude Include="src\fheroes2\editor\editor_sphinx_window.h" />
331333
<ClInclude Include="src\fheroes2\editor\editor_ui_helper.h" />
332334
<ClInclude Include="src\fheroes2\editor\history_manager.h" />

src/fheroes2/editor/editor_interface.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#include "editor_map_specs_window.h"
4545
#include "editor_object_popup_window.h"
4646
#include "editor_save_map_window.h"
47+
#include "editor_spell_selection.h"
4748
#include "editor_sphinx_window.h"
4849
#include "game.h"
4950
#include "game_delays.h"
@@ -397,6 +398,25 @@ namespace
397398

398399
needRedraw = true;
399400
}
401+
else if ( objectIter->group == Maps::ObjectGroup::ADVENTURE_POWER_UPS ) {
402+
const auto & objects = Maps::getObjectsByGroup( objectIter->group );
403+
404+
assert( objectIter->index < objects.size() );
405+
const auto objectType = objects[objectIter->index].objectType;
406+
switch ( objectType ) {
407+
case MP2::OBJ_SHRINE_FIRST_CIRCLE:
408+
case MP2::OBJ_SHRINE_SECOND_CIRCLE:
409+
case MP2::OBJ_SHRINE_THIRD_CIRCLE:
410+
// We cannot assert non-existing metadata as these objects could have been created by an older Editor version.
411+
mapFormat.shrineMetadata.erase( objectIter->id );
412+
break;
413+
default:
414+
break;
415+
}
416+
417+
objectIter = mapTile.objects.erase( objectIter );
418+
needRedraw = true;
419+
}
400420
else {
401421
objectIter = mapTile.objects.erase( objectIter );
402422
needRedraw = true;
@@ -1343,6 +1363,37 @@ namespace Interface
13431363
action.commit();
13441364
}
13451365
}
1366+
else if ( objectType == MP2::OBJ_SHRINE_FIRST_CIRCLE || objectType == MP2::OBJ_SHRINE_SECOND_CIRCLE || objectType == MP2::OBJ_SHRINE_THIRD_CIRCLE ) {
1367+
auto shrineMetadata = _mapFormat.shrineMetadata.find( object.id );
1368+
if ( shrineMetadata == _mapFormat.shrineMetadata.end() ) {
1369+
_mapFormat.shrineMetadata[object.id] = {};
1370+
}
1371+
1372+
auto & originalMetadata = _mapFormat.shrineMetadata[object.id];
1373+
auto newMetadata = originalMetadata;
1374+
1375+
int spellLevel = 0;
1376+
if ( objectType == MP2::OBJ_SHRINE_FIRST_CIRCLE ) {
1377+
spellLevel = 1;
1378+
}
1379+
else if ( objectType == MP2::OBJ_SHRINE_SECOND_CIRCLE ) {
1380+
spellLevel = 2;
1381+
}
1382+
else if ( objectType == MP2::OBJ_SHRINE_THIRD_CIRCLE ) {
1383+
spellLevel = 3;
1384+
}
1385+
else {
1386+
assert( 0 );
1387+
spellLevel = 1;
1388+
}
1389+
1390+
if ( Editor::openSpellSelectionWindow( MP2::StringObject( objectType ), spellLevel, newMetadata.allowedSpells )
1391+
&& originalMetadata.allowedSpells != newMetadata.allowedSpells ) {
1392+
fheroes2::ActionCreator action( _historyManager, _mapFormat );
1393+
originalMetadata = std::move( newMetadata );
1394+
action.commit();
1395+
}
1396+
}
13461397
else {
13471398
std::string msg = _( "%{object} has no properties to change." );
13481399
StringReplace( msg, "%{object}", MP2::StringObject( objectType ) );
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
/***************************************************************************
2+
* fheroes2: https://github.com/ihhub/fheroes2 *
3+
* Copyright (C) 2024 *
4+
* *
5+
* This program is free software; you can redistribute it and/or modify *
6+
* it under the terms of the GNU General Public License as published by *
7+
* the Free Software Foundation; either version 2 of the License, or *
8+
* (at your option) any later version. *
9+
* *
10+
* This program is distributed in the hope that it will be useful, *
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13+
* GNU General Public License for more details. *
14+
* *
15+
* You should have received a copy of the GNU General Public License *
16+
* along with this program; if not, write to the *
17+
* Free Software Foundation, Inc., *
18+
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
19+
***************************************************************************/
20+
21+
#include "editor_spell_selection.h"
22+
23+
#include <algorithm>
24+
#include <cassert>
25+
#include <cstddef>
26+
#include <initializer_list>
27+
#include <utility>
28+
29+
#include "agg_image.h"
30+
#include "cursor.h"
31+
#include "dialog.h"
32+
#include "game_hotkeys.h"
33+
#include "icn.h"
34+
#include "image.h"
35+
#include "localevent.h"
36+
#include "math_base.h"
37+
#include "math_tools.h"
38+
#include "pal.h"
39+
#include "screen.h"
40+
#include "settings.h"
41+
#include "spell.h"
42+
#include "translations.h"
43+
#include "ui_button.h"
44+
#include "ui_dialog.h"
45+
#include "ui_text.h"
46+
#include "ui_window.h"
47+
48+
namespace
49+
{
50+
const int32_t spellRowOffsetY{ 90 };
51+
const int32_t spellItemWidth{ 110 };
52+
53+
// Up to 5 spells can be displayed in a row.
54+
// Up to 5 rows can be displayed.
55+
// So far the number of spells of any level is much more than 25.
56+
class SpellContainerUI final
57+
{
58+
public:
59+
SpellContainerUI( fheroes2::Point offset, std::vector<std::pair<Spell, bool>> & spells )
60+
: _spells( spells )
61+
{
62+
assert( !spells.empty() && spells.size() < 25 );
63+
64+
// Figure out how many rows and columns we want to display.
65+
for ( size_t i = 1; i < 5; ++i ) {
66+
if ( i * i >= _spells.size() ) {
67+
_spellsPerRow = i;
68+
break;
69+
}
70+
}
71+
72+
offset.x += ( fheroes2::Display::DEFAULT_WIDTH - static_cast<int32_t>( _spellsPerRow ) * spellItemWidth ) / 2;
73+
offset.y += ( fheroes2::Display::DEFAULT_HEIGHT - 50 - spellRowOffsetY * static_cast<int32_t>( _spells.size() / _spellsPerRow ) ) / 2;
74+
75+
// Calculate all areas where we are going to render spells.
76+
_spellRoi.reserve( _spells.size() );
77+
78+
const fheroes2::Sprite & scrollImage = fheroes2::AGG::GetICN( ICN::TOWNWIND, 0 );
79+
80+
const int32_t lastRowColumns = static_cast<int32_t>( _spells.size() % _spellsPerRow );
81+
const int32_t lastRowOffsetX = ( lastRowColumns > 0 ) ? ( static_cast<int32_t>( _spellsPerRow ) - lastRowColumns ) * spellItemWidth / 2 : 0;
82+
83+
for ( size_t i = 0; i < _spells.size(); ++i ) {
84+
const int32_t rowId = static_cast<int32_t>( i / _spellsPerRow );
85+
const int32_t columnId = static_cast<int32_t>( i % _spellsPerRow );
86+
87+
if ( rowId == static_cast<int32_t>( _spells.size() / _spellsPerRow ) ) {
88+
// This is the last row.
89+
_spellRoi.emplace_back( offset.x + columnId * spellItemWidth + lastRowOffsetX, offset.y + rowId * spellRowOffsetY, scrollImage.width(),
90+
scrollImage.height() );
91+
}
92+
else {
93+
_spellRoi.emplace_back( offset.x + columnId * spellItemWidth, offset.y + rowId * spellRowOffsetY, scrollImage.width(), scrollImage.height() );
94+
}
95+
}
96+
}
97+
98+
void draw( fheroes2::Image & output )
99+
{
100+
const fheroes2::Sprite & scrollImage = fheroes2::AGG::GetICN( ICN::TOWNWIND, 0 );
101+
102+
fheroes2::Sprite inactiveScrollImage( scrollImage );
103+
fheroes2::ApplyPalette( inactiveScrollImage, PAL::GetPalette( PAL::PaletteType::GRAY ) );
104+
105+
for ( size_t i = 0; i < _spells.size(); ++i ) {
106+
if ( !_spells[i].second ) {
107+
// The spell is being inactive.
108+
fheroes2::Blit( inactiveScrollImage, output, _spellRoi[i].x, _spellRoi[i].y );
109+
}
110+
else {
111+
fheroes2::Blit( scrollImage, output, _spellRoi[i].x, _spellRoi[i].y );
112+
}
113+
114+
const fheroes2::Sprite & spellImage = fheroes2::AGG::GetICN( ICN::SPELLS, _spells[i].first.IndexSprite() );
115+
116+
if ( !_spells[i].second ) {
117+
// The spell is being inactive.
118+
fheroes2::Sprite inactiveSpellImage( spellImage );
119+
fheroes2::ApplyPalette( inactiveSpellImage, PAL::GetPalette( PAL::PaletteType::GRAY ) );
120+
121+
fheroes2::Blit( inactiveSpellImage, output, _spellRoi[i].x + 3 + ( _spellRoi[i].width - inactiveSpellImage.width() ) / 2,
122+
_spellRoi[i].y + 31 - inactiveSpellImage.height() / 2 );
123+
}
124+
else {
125+
fheroes2::Blit( spellImage, output, _spellRoi[i].x + 3 + ( _spellRoi[i].width - spellImage.width() ) / 2,
126+
_spellRoi[i].y + 31 - spellImage.height() / 2 );
127+
}
128+
129+
const fheroes2::Text text( _spells[i].first.GetName(), fheroes2::FontType::smallWhite() );
130+
text.draw( _spellRoi[i].x + 18, _spellRoi[i].y + 57, 78, fheroes2::Display::instance() );
131+
}
132+
}
133+
134+
bool processEvents( LocalEvent & eventProcessor )
135+
{
136+
const int32_t spellIndex = GetRectIndex( _spellRoi, eventProcessor.getMouseCursorPos() );
137+
if ( spellIndex < 0 ) {
138+
return false;
139+
}
140+
141+
if ( eventProcessor.MouseClickLeft() ) {
142+
assert( static_cast<size_t>( spellIndex ) < _spellRoi.size() );
143+
144+
_spells[spellIndex].second = !_spells[spellIndex].second;
145+
return true;
146+
}
147+
148+
if ( eventProcessor.isMouseRightButtonPressed() ) {
149+
fheroes2::SpellDialogElement( _spells[spellIndex].first, nullptr ).showPopup( Dialog::ZERO );
150+
}
151+
152+
return false;
153+
}
154+
155+
private:
156+
std::vector<std::pair<Spell, bool>> & _spells;
157+
158+
std::vector<fheroes2::Rect> _spellRoi;
159+
160+
size_t _spellsPerRow{ 0 };
161+
};
162+
}
163+
164+
namespace Editor
165+
{
166+
bool openSpellSelectionWindow( std::string title, const int spellLevel, std::vector<int32_t> & selectedSpells )
167+
{
168+
if ( spellLevel < 1 || spellLevel > 5 ) {
169+
// What are you trying to achieve?!
170+
assert( 0 );
171+
return false;
172+
}
173+
174+
const std::vector<int32_t> & availableSpells = Spell::getAllSpellIdsSuitableForSpellBook( spellLevel );
175+
assert( !availableSpells.empty() );
176+
177+
// Create a container of active and disabled spells.
178+
std::vector<std::pair<Spell, bool>> spells;
179+
spells.reserve( availableSpells.size() );
180+
181+
bool isAnySpellEnabled = false;
182+
183+
for ( const int & spell : availableSpells ) {
184+
const bool isSelected = ( std::find( selectedSpells.begin(), selectedSpells.end(), spell ) != selectedSpells.end() );
185+
186+
spells.emplace_back( spell, isSelected );
187+
188+
if ( isSelected ) {
189+
isAnySpellEnabled = true;
190+
}
191+
}
192+
193+
// If no spells are selected, select all of them.
194+
if ( !isAnySpellEnabled ) {
195+
for ( auto & [spell, isSelected] : spells ) {
196+
isSelected = true;
197+
}
198+
}
199+
200+
const CursorRestorer cursorRestorer( true, Cursor::POINTER );
201+
202+
fheroes2::Display & display = fheroes2::Display::instance();
203+
204+
const bool isDefaultScreenSize = display.isDefaultSize();
205+
206+
fheroes2::StandardWindow background( fheroes2::Display::DEFAULT_WIDTH, fheroes2::Display::DEFAULT_HEIGHT, !isDefaultScreenSize );
207+
const fheroes2::Rect activeArea( background.activeArea() );
208+
209+
const bool isEvilInterface = Settings::Get().isEvilInterfaceEnabled();
210+
211+
if ( isDefaultScreenSize ) {
212+
const fheroes2::Sprite & backgroundImage = fheroes2::AGG::GetICN( isEvilInterface ? ICN::STONEBAK_EVIL : ICN::STONEBAK, 0 );
213+
fheroes2::Copy( backgroundImage, 0, 0, display, activeArea );
214+
}
215+
216+
const fheroes2::Text text( std::move( title ), fheroes2::FontType::normalYellow() );
217+
text.draw( activeArea.x + ( activeArea.width - text.width() ) / 2, activeArea.y + 10, display );
218+
219+
// Buttons.
220+
fheroes2::Button buttonOk;
221+
fheroes2::Button buttonCancel;
222+
223+
background.renderOkayCancelButtons( buttonOk, buttonCancel, isEvilInterface );
224+
225+
fheroes2::ImageRestorer restorer( display, activeArea.x, activeArea.y, activeArea.width, activeArea.height );
226+
227+
SpellContainerUI spellContainer( activeArea.getPosition(), spells );
228+
229+
spellContainer.draw( display );
230+
231+
display.render( background.totalArea() );
232+
233+
LocalEvent & le = LocalEvent::Get();
234+
while ( le.HandleEvents() ) {
235+
if ( buttonOk.isEnabled() ) {
236+
buttonOk.drawOnState( le.isMouseLeftButtonPressedInArea( buttonOk.area() ) );
237+
}
238+
239+
buttonCancel.drawOnState( le.isMouseLeftButtonPressedInArea( buttonCancel.area() ) );
240+
241+
if ( Game::HotKeyPressEvent( Game::HotKeyEvent::DEFAULT_CANCEL ) || le.MouseClickLeft( buttonCancel.area() ) ) {
242+
return false;
243+
}
244+
245+
if ( buttonOk.isEnabled() && ( Game::HotKeyPressEvent( Game::HotKeyEvent::DEFAULT_OKAY ) || le.MouseClickLeft( buttonOk.area() ) ) ) {
246+
break;
247+
}
248+
249+
if ( le.isMouseRightButtonPressedInArea( buttonCancel.area() ) ) {
250+
fheroes2::showStandardTextMessage( _( "Cancel" ), _( "Exit this menu without doing anything." ), Dialog::ZERO );
251+
}
252+
else if ( le.isMouseRightButtonPressedInArea( buttonOk.area() ) ) {
253+
fheroes2::showStandardTextMessage( _( "Okay" ), _( "Click to accept the changes made." ), Dialog::ZERO );
254+
}
255+
256+
if ( spellContainer.processEvents( le ) ) {
257+
restorer.restore();
258+
259+
spellContainer.draw( display );
260+
261+
// Check if all spells are being disabled. If they are disable the OKAY button.
262+
bool areAllSpelledDisabled = true;
263+
for ( const auto & [spell, isSelected] : spells ) {
264+
if ( isSelected ) {
265+
areAllSpelledDisabled = false;
266+
break;
267+
}
268+
}
269+
270+
if ( areAllSpelledDisabled ) {
271+
buttonOk.disable();
272+
buttonOk.draw();
273+
}
274+
else {
275+
buttonOk.enable();
276+
buttonOk.draw();
277+
}
278+
279+
display.render( activeArea );
280+
}
281+
}
282+
283+
selectedSpells.clear();
284+
285+
for ( const auto & [spell, isSelected] : spells ) {
286+
if ( isSelected ) {
287+
selectedSpells.emplace_back( spell.GetID() );
288+
}
289+
}
290+
291+
// 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.
292+
if ( selectedSpells.size() == spells.size() ) {
293+
selectedSpells = {};
294+
}
295+
296+
return true;
297+
}
298+
}

0 commit comments

Comments
 (0)