1 /***************************************************************************
2  *   Copyright (C) 2009 by Andrey Afletdinov <fheroes2@gmail.com>          *
3  *                                                                         *
4  *   Part of the Free Heroes2 Engine:                                      *
5  *   http://sourceforge.net/projects/fheroes2                              *
6  *                                                                         *
7  *   This program is free software; you can redistribute it and/or modify  *
8  *   it under the terms of the GNU General Public License as published by  *
9  *   the Free Software Foundation; either version 2 of the License, or     *
10  *   (at your option) any later version.                                   *
11  *                                                                         *
12  *   This program is distributed in the hope that it will be useful,       *
13  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
14  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
15  *   GNU General Public License for more details.                          *
16  *                                                                         *
17  *   You should have received a copy of the GNU General Public License     *
18  *   along with this program; if not, write to the                         *
19  *   Free Software Foundation, Inc.,                                       *
20  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
21  ***************************************************************************/
22 
23 #include <cassert>
24 
25 #include "agg_image.h"
26 #include "army.h"
27 #include "battle.h"
28 #include "battle_cell.h"
29 #include "cursor.h"
30 #include "dialog.h"
31 #include "game.h"
32 #include "game_delays.h"
33 #include "icn.h"
34 #include "luck.h"
35 #include "monster.h"
36 #include "monster_anim.h"
37 #include "morale.h"
38 #include "payment.h"
39 #include "settings.h"
40 #include "text.h"
41 #include "tools.h"
42 #include "translations.h"
43 #include "ui_button.h"
44 #include "ui_text.h"
45 #include "world.h"
46 
47 namespace
48 {
49     const int offsetXAmountBox = 80;
50     const int offsetYAmountBox = 223;
51     const int widthAmountBox = 125;
52     const int heightAmountBox = 23;
53 
54     struct SpellInfo
55     {
SpellInfo__anonde09e35c0111::SpellInfo56         SpellInfo( const uint32_t mode_, const uint32_t duration_, const int32_t offset_, const int32_t space_ )
57             : mode( mode_ )
58             , duration( duration_ )
59             , offset( offset_ )
60             , space( space_ )
61         {}
62 
63         uint32_t mode;
64         uint32_t duration;
65         int32_t offset;
66         int32_t space;
67         Spell spell;
68     };
69 
modeToSpell(const uint32_t modeId)70     Spell modeToSpell( const uint32_t modeId )
71     {
72         switch ( modeId ) {
73         case Battle::SP_BLOODLUST:
74             return Spell::BLOODLUST;
75         case Battle::SP_BLESS:
76             return Spell::BLESS;
77         case Battle::SP_HASTE:
78             return Spell::HASTE;
79         case Battle::SP_SHIELD:
80             return Spell::SHIELD;
81         case Battle::SP_STONESKIN:
82             return Spell::STONESKIN;
83         case Battle::SP_DRAGONSLAYER:
84             return Spell::DRAGONSLAYER;
85         case Battle::SP_STEELSKIN:
86             return Spell::STEELSKIN;
87         case Battle::SP_ANTIMAGIC:
88             return Spell::ANTIMAGIC;
89         case Battle::SP_CURSE:
90             return Spell::CURSE;
91         case Battle::SP_SLOW:
92             return Spell::SLOW;
93         case Battle::SP_BERSERKER:
94             return Spell::BERSERKER;
95         case Battle::SP_HYPNOTIZE:
96             return Spell::HYPNOTIZE;
97         case Battle::SP_BLIND:
98             return Spell::BLIND;
99         case Battle::SP_PARALYZE:
100             return Spell::PARALYZE;
101         case Battle::SP_STONE:
102             return Spell::STONE;
103         default:
104             // Did you add another mode? Please add a corresponding spell.
105             assert( 0 );
106             break;
107         }
108 
109         return Spell::NONE;
110     }
111 }
112 
113 void DrawMonsterStats( const fheroes2::Point & dst, const Troop & troop );
114 std::vector<std::pair<fheroes2::Rect, Spell>> DrawBattleStats( const fheroes2::Point & dst, const Troop & b );
115 void DrawMonsterInfo( const fheroes2::Point & dst, const Troop & troop );
116 void DrawMonster( fheroes2::RandomMonsterAnimation & monsterAnimation, const Troop & troop, const fheroes2::Point & offset, bool isReflected, bool isAnimated,
117                   const fheroes2::Rect & roi );
118 
ArmyInfo(const Troop & troop,int flags,bool isReflected)119 int Dialog::ArmyInfo( const Troop & troop, int flags, bool isReflected )
120 {
121     // The active size of the window is 520 by 256 pixels
122     fheroes2::Display & display = fheroes2::Display::instance();
123     const bool isEvilInterface = Settings::Get().ExtGameEvilInterface();
124 
125     const int viewarmy = isEvilInterface ? ICN::VIEWARME : ICN::VIEWARMY;
126     const fheroes2::Sprite & sprite_dialog = fheroes2::AGG::GetICN( viewarmy, 0 );
127     const fheroes2::Sprite & spriteDialogShadow = fheroes2::AGG::GetICN( viewarmy, 7 );
128 
129     // setup cursor
130     const CursorRestorer cursorRestorer( ( flags & BUTTONS ) != 0, Cursor::POINTER );
131 
132     fheroes2::Point dialogOffset( ( display.width() - sprite_dialog.width() ) / 2, ( display.height() - sprite_dialog.height() ) / 2 );
133     if ( isEvilInterface ) {
134         dialogOffset.y += 3;
135     }
136 
137     const fheroes2::Point shadowShift( spriteDialogShadow.x() - sprite_dialog.x(), spriteDialogShadow.y() - sprite_dialog.y() );
138     const fheroes2::Point shadowOffset( dialogOffset.x + shadowShift.x, dialogOffset.y + shadowShift.y );
139 
140     fheroes2::ImageRestorer restorer( display, shadowOffset.x, dialogOffset.y, sprite_dialog.width() - shadowShift.x, sprite_dialog.height() + shadowShift.y );
141     fheroes2::Blit( spriteDialogShadow, display, dialogOffset.x + shadowShift.x, dialogOffset.y + shadowShift.y );
142     fheroes2::Blit( sprite_dialog, display, dialogOffset.x, dialogOffset.y );
143 
144     fheroes2::Rect pos_rt( dialogOffset.x, dialogOffset.y, sprite_dialog.width(), sprite_dialog.height() );
145     if ( isEvilInterface ) {
146         pos_rt.x += 9;
147         pos_rt.y -= 1;
148     }
149 
150     const fheroes2::Point monsterStatOffset( pos_rt.x + 400, pos_rt.y + 37 );
151     DrawMonsterStats( monsterStatOffset, troop );
152 
153     std::vector<std::pair<fheroes2::Rect, Spell>> spellAreas;
154 
155     const fheroes2::Point battleStatOffset( pos_rt.x + 395, pos_rt.y + 184 );
156     if ( troop.isBattle() )
157         spellAreas = DrawBattleStats( battleStatOffset, troop );
158 
159     DrawMonsterInfo( pos_rt.getPosition(), troop );
160 
161     const bool isAnimated = ( flags & BUTTONS ) != 0;
162     fheroes2::RandomMonsterAnimation monsterAnimation( troop );
163     const fheroes2::Point monsterOffset( pos_rt.x + 520 / 4 + 16, pos_rt.y + 175 );
164     if ( !isAnimated )
165         monsterAnimation.reset();
166 
167     const fheroes2::Rect dialogRoi( pos_rt.x, pos_rt.y + SHADOWWIDTH, sprite_dialog.width(), sprite_dialog.height() - 2 * SHADOWWIDTH );
168     DrawMonster( monsterAnimation, troop, monsterOffset, isReflected, isAnimated, dialogRoi );
169 
170     // button upgrade
171     fheroes2::Point dst_pt( pos_rt.x + 400, pos_rt.y + 40 );
172     dst_pt.x = pos_rt.x + 280;
173     dst_pt.y = pos_rt.y + 192;
174     fheroes2::Button buttonUpgrade( dst_pt.x, dst_pt.y, viewarmy, 5, 6 );
175 
176     // button dismiss
177     dst_pt.x = pos_rt.x + 280;
178     dst_pt.y = pos_rt.y + 221;
179     fheroes2::Button buttonDismiss( dst_pt.x, dst_pt.y, viewarmy, 1, 2 );
180 
181     // button exit
182     dst_pt.x = pos_rt.x + 415;
183     dst_pt.y = pos_rt.y + 221;
184     fheroes2::Button buttonExit( dst_pt.x, dst_pt.y, viewarmy, 3, 4 );
185 
186     if ( READONLY & flags ) {
187         buttonDismiss.disable();
188     }
189 
190     if ( !troop.isBattle() && troop.isAllowUpgrade() && ( UPGRADE & flags ) ) {
191         buttonUpgrade.enable();
192         buttonUpgrade.draw();
193     }
194     else
195         buttonUpgrade.disable();
196 
197     if ( BUTTONS & flags ) {
198         if ( !troop.isBattle() && !( READONLY & flags ) )
199             buttonDismiss.draw();
200         buttonExit.draw();
201     }
202 
203     LocalEvent & le = LocalEvent::Get();
204     int result = Dialog::ZERO;
205 
206     display.render();
207 
208     // dialog menu loop
209     while ( le.HandleEvents() ) {
210         if ( flags & BUTTONS ) {
211             if ( buttonUpgrade.isEnabled() )
212                 le.MousePressLeft( buttonUpgrade.area() ) ? buttonUpgrade.drawOnPress() : buttonUpgrade.drawOnRelease();
213             if ( buttonDismiss.isEnabled() )
214                 le.MousePressLeft( buttonDismiss.area() ) ? buttonDismiss.drawOnPress() : buttonDismiss.drawOnRelease();
215             le.MousePressLeft( buttonExit.area() ) ? buttonExit.drawOnPress() : buttonExit.drawOnRelease();
216 
217             // upgrade
218             if ( buttonUpgrade.isEnabled() && ( le.MouseClickLeft( buttonUpgrade.area() ) || Game::HotKeyPressEvent( Game::EVENT_UPGRADE_TROOP ) ) ) {
219                 if ( UPGRADE_DISABLE & flags ) {
220                     const std::string msg( _( "You can't afford to upgrade your troops!" ) );
221                     if ( Dialog::YES == Dialog::ResourceInfo( "", msg, troop.GetUpgradeCost(), Dialog::OK ) ) {
222                         result = Dialog::UPGRADE;
223                         break;
224                     }
225                 }
226                 else {
227                     const std::string msg = _( "Your troops can be upgraded, but it will cost you dearly. Do you wish to upgrade them?" );
228 
229                     if ( Dialog::YES == Dialog::ResourceInfo( "", msg, troop.GetUpgradeCost(), Dialog::YES | Dialog::NO ) ) {
230                         result = Dialog::UPGRADE;
231                         break;
232                     }
233                 }
234             }
235             // dismiss
236             if ( buttonDismiss.isEnabled() && ( le.MouseClickLeft( buttonDismiss.area() ) || Game::HotKeyPressEvent( Game::EVENT_DISMISS_TROOP ) )
237                  && Dialog::YES
238                         == Dialog::Message( troop.GetPluralName( troop.GetCount() ), _( "Are you sure you want to dismiss this army?" ), Font::BIG,
239                                             Dialog::YES | Dialog::NO ) ) {
240                 result = Dialog::DISMISS;
241                 break;
242             }
243             // exit
244             if ( le.MouseClickLeft( buttonExit.area() ) || HotKeyCloseWindow ) {
245                 result = Dialog::CANCEL;
246                 break;
247             }
248 
249             for ( const auto & spellInfo : spellAreas ) {
250                 if ( le.MousePressRight( spellInfo.first ) ) {
251                     Dialog::SpellInfo( spellInfo.second, nullptr, false );
252                     break;
253                 }
254             }
255 
256             if ( Game::validateAnimationDelay( Game::CASTLE_UNIT_DELAY ) ) {
257                 fheroes2::Blit( sprite_dialog, display, dialogOffset.x, dialogOffset.y );
258 
259                 DrawMonsterStats( monsterStatOffset, troop );
260 
261                 if ( troop.isBattle() )
262                     spellAreas = DrawBattleStats( battleStatOffset, troop );
263 
264                 DrawMonsterInfo( pos_rt.getPosition(), troop );
265                 DrawMonster( monsterAnimation, troop, monsterOffset, isReflected, true, dialogRoi );
266 
267                 if ( buttonUpgrade.isEnabled() )
268                     buttonUpgrade.draw();
269 
270                 if ( buttonDismiss.isEnabled() )
271                     buttonDismiss.draw();
272 
273                 if ( buttonExit.isEnabled() )
274                     buttonExit.draw();
275 
276                 display.render();
277             }
278         }
279         else {
280             if ( !le.MousePressRight() )
281                 break;
282         }
283     }
284 
285     return result;
286 }
287 
DrawMonsterStats(const fheroes2::Point & dst,const Troop & troop)288 void DrawMonsterStats( const fheroes2::Point & dst, const Troop & troop )
289 {
290     fheroes2::Point dst_pt;
291     Text text;
292 
293     // attack
294     text.Set( std::string( _( "Attack Skill" ) ) + ":" );
295     dst_pt.x = dst.x - text.w();
296     dst_pt.y = dst.y;
297     text.Blit( dst_pt.x, dst_pt.y );
298 
299     const int offsetX = 6;
300     const int offsetY = 16;
301 
302     text.Set( troop.GetAttackString() );
303     dst_pt.x = dst.x + offsetX;
304     text.Blit( dst_pt.x, dst_pt.y );
305 
306     // defense
307     text.Set( std::string( _( "Defense Skill" ) ) + ":" );
308     dst_pt.x = dst.x - text.w();
309     dst_pt.y += offsetY;
310     text.Blit( dst_pt.x, dst_pt.y );
311 
312     text.Set( troop.GetDefenseString() );
313     dst_pt.x = dst.x + offsetX;
314     text.Blit( dst_pt.x, dst_pt.y );
315 
316     // shot
317     if ( troop.isArchers() ) {
318         std::string message = troop.isBattle() ? _( "Shots Left" ) : _( "Shots" );
319         message += ':';
320         text.Set( message );
321         dst_pt.x = dst.x - text.w();
322         dst_pt.y += offsetY;
323         text.Blit( dst_pt.x, dst_pt.y );
324 
325         text.Set( troop.GetShotString() );
326         dst_pt.x = dst.x + offsetX;
327         text.Blit( dst_pt.x, dst_pt.y );
328     }
329 
330     // damage
331     text.Set( std::string( _( "Damage" ) ) + ":" );
332     dst_pt.x = dst.x - text.w();
333     dst_pt.y += offsetY;
334     text.Blit( dst_pt.x, dst_pt.y );
335 
336     if ( troop.GetMonster().GetDamageMin() != troop.GetMonster().GetDamageMax() )
337         text.Set( std::to_string( troop.GetMonster().GetDamageMin() ) + "-" + std::to_string( troop.GetMonster().GetDamageMax() ) );
338     else
339         text.Set( std::to_string( troop.GetMonster().GetDamageMin() ) );
340     dst_pt.x = dst.x + offsetX;
341     text.Blit( dst_pt.x, dst_pt.y );
342 
343     // hp
344     text.Set( std::string( _( "Hit Points" ) ) + ":" );
345     dst_pt.x = dst.x - text.w();
346     dst_pt.y += offsetY;
347     text.Blit( dst_pt.x, dst_pt.y );
348 
349     text.Set( std::to_string( troop.GetMonster().GetHitPoints() ) );
350     dst_pt.x = dst.x + offsetX;
351     text.Blit( dst_pt.x, dst_pt.y );
352 
353     if ( troop.isBattle() ) {
354         text.Set( std::string( _( "Hit Points Left" ) ) + ":" );
355         dst_pt.x = dst.x - text.w();
356         dst_pt.y += offsetY;
357         text.Blit( dst_pt.x, dst_pt.y );
358 
359         text.Set( std::to_string( troop.GetHitPointsLeft() ) );
360         dst_pt.x = dst.x + offsetX;
361         text.Blit( dst_pt.x, dst_pt.y );
362     }
363 
364     // speed
365     text.Set( std::string( _( "Speed" ) ) + ":" );
366     dst_pt.x = dst.x - text.w();
367     dst_pt.y += offsetY;
368     text.Blit( dst_pt.x, dst_pt.y );
369 
370     text.Set( troop.GetSpeedString() );
371     dst_pt.x = dst.x + offsetX;
372     text.Blit( dst_pt.x, dst_pt.y );
373 
374     // morale
375     text.Set( std::string( _( "Morale" ) ) + ":" );
376     dst_pt.x = dst.x - text.w();
377     dst_pt.y += offsetY;
378     text.Blit( dst_pt.x, dst_pt.y );
379 
380     text.Set( Morale::String( troop.GetMorale() ) );
381     dst_pt.x = dst.x + offsetX;
382     text.Blit( dst_pt.x, dst_pt.y );
383 
384     // luck
385     text.Set( std::string( _( "Luck" ) ) + ":" );
386     dst_pt.x = dst.x - text.w();
387     dst_pt.y += offsetY;
388     text.Blit( dst_pt.x, dst_pt.y );
389 
390     text.Set( Luck::String( troop.GetLuck() ) );
391     dst_pt.x = dst.x + offsetX;
392     text.Blit( dst_pt.x, dst_pt.y );
393 }
394 
GetModesSprite(u32 mod)395 fheroes2::Sprite GetModesSprite( u32 mod )
396 {
397     switch ( mod ) {
398     case Battle::SP_BLOODLUST:
399         return fheroes2::AGG::GetICN( ICN::SPELLINL, 9 );
400     case Battle::SP_BLESS:
401         return fheroes2::AGG::GetICN( ICN::SPELLINL, 3 );
402     case Battle::SP_HASTE:
403         return fheroes2::AGG::GetICN( ICN::SPELLINL, 0 );
404     case Battle::SP_SHIELD:
405         return fheroes2::AGG::GetICN( ICN::SPELLINL, 10 );
406     case Battle::SP_STONESKIN:
407         return fheroes2::AGG::GetICN( ICN::SPELLINL, 13 );
408     case Battle::SP_DRAGONSLAYER:
409         return fheroes2::AGG::GetICN( ICN::SPELLINL, 8 );
410     case Battle::SP_STEELSKIN:
411         return fheroes2::AGG::GetICN( ICN::SPELLINL, 14 );
412     case Battle::SP_ANTIMAGIC:
413         return fheroes2::AGG::GetICN( ICN::SPELLINL, 12 );
414     case Battle::SP_CURSE:
415         return fheroes2::AGG::GetICN( ICN::SPELLINL, 4 );
416     case Battle::SP_SLOW:
417         return fheroes2::AGG::GetICN( ICN::SPELLINL, 1 );
418     case Battle::SP_BERSERKER:
419         return fheroes2::AGG::GetICN( ICN::SPELLINL, 5 );
420     case Battle::SP_HYPNOTIZE:
421         return fheroes2::AGG::GetICN( ICN::SPELLINL, 7 );
422     case Battle::SP_BLIND:
423         return fheroes2::AGG::GetICN( ICN::SPELLINL, 2 );
424     case Battle::SP_PARALYZE:
425         return fheroes2::AGG::GetICN( ICN::SPELLINL, 6 );
426     case Battle::SP_STONE:
427         return fheroes2::AGG::GetICN( ICN::SPELLINL, 11 );
428     default:
429         break;
430     }
431 
432     return fheroes2::Sprite();
433 }
434 
DrawBattleStats(const fheroes2::Point & dst,const Troop & b)435 std::vector<std::pair<fheroes2::Rect, Spell>> DrawBattleStats( const fheroes2::Point & dst, const Troop & b )
436 {
437     std::vector<std::pair<fheroes2::Rect, Spell>> output;
438 
439     const uint32_t modes[] = { Battle::SP_BLOODLUST,    Battle::SP_BLESS,     Battle::SP_HASTE,     Battle::SP_SHIELD,   Battle::SP_STONESKIN,
440                                Battle::SP_DRAGONSLAYER, Battle::SP_STEELSKIN, Battle::SP_ANTIMAGIC, Battle::SP_CURSE,    Battle::SP_SLOW,
441                                Battle::SP_BERSERKER,    Battle::SP_HYPNOTIZE, Battle::SP_BLIND,     Battle::SP_PARALYZE, Battle::SP_STONE };
442 
443     int32_t ow = 0;
444     int32_t spritesWidth = 0;
445 
446     std::vector<SpellInfo> spellsInfo;
447     for ( const uint32_t mode : modes ) {
448         if ( !b.isModes( mode ) )
449             continue;
450 
451         const fheroes2::Sprite & sprite = GetModesSprite( mode );
452         if ( sprite.empty() )
453             continue;
454 
455         const uint32_t duration = b.GetAffectedDuration( mode );
456         int offset = 0;
457         if ( duration > 0 ) {
458             offset = duration >= 10 ? 12 : 7;
459             if ( mode >= Battle::SP_BLESS && mode <= Battle::SP_DRAGONSLAYER )
460                 offset -= 5;
461         }
462         const int space = ( offset == 2 ) ? 10 : 5;
463 
464         spellsInfo.emplace_back( mode, duration, offset, space );
465         ow += sprite.width() + offset + space;
466         spritesWidth += sprite.width();
467     }
468 
469     if ( spellsInfo.empty() )
470         return output;
471 
472     std::sort( spellsInfo.begin(), spellsInfo.end(),
473                []( const SpellInfo & first, const SpellInfo & second ) { return first.duration > 0 && first.duration < second.duration; } );
474 
475     ow -= spellsInfo.back().space;
476 
477     const int maxSpritesWidth = 212;
478     const int maxSpriteHeight = 32;
479 
480     Text text;
481     if ( ow <= maxSpritesWidth ) {
482         ow = dst.x - ow / 2;
483         for ( const auto & spell : spellsInfo ) {
484             const fheroes2::Sprite & sprite = GetModesSprite( spell.mode );
485             const fheroes2::Point imageOffset( ow, dst.y + maxSpriteHeight - sprite.height() );
486 
487             fheroes2::Blit( sprite, fheroes2::Display::instance(), imageOffset.x, imageOffset.y );
488             output.emplace_back( std::make_pair( fheroes2::Rect( imageOffset.x, imageOffset.y, sprite.width(), sprite.height() ), modeToSpell( spell.mode ) ) );
489 
490             if ( spell.duration > 0 ) {
491                 text.Set( std::to_string( spell.duration ), Font::SMALL );
492                 ow += sprite.width() + spell.offset;
493                 text.Blit( ow - text.w(), dst.y + maxSpriteHeight - text.h() + 1 );
494             }
495             ow += spell.space;
496         }
497     }
498     else {
499         // Too many spells
500         const int widthDiff = maxSpritesWidth - spritesWidth;
501         int space = widthDiff / static_cast<int>( spellsInfo.size() - 1 );
502         if ( widthDiff > 0 ) {
503             if ( space > 10 )
504                 space = 10;
505             ow = dst.x + ( spritesWidth + space * static_cast<int>( spellsInfo.size() - 1 ) ) / 2;
506         }
507         else {
508             ow = dst.x + maxSpritesWidth / 2;
509         }
510 
511         for ( auto spellIt = spellsInfo.crbegin(); spellIt != spellsInfo.crend(); ++spellIt ) {
512             const fheroes2::Sprite & sprite = GetModesSprite( spellIt->mode );
513             const fheroes2::Point imageOffset( ow - sprite.width(), dst.y + maxSpriteHeight - sprite.height() );
514 
515             fheroes2::Blit( sprite, fheroes2::Display::instance(), imageOffset.x, imageOffset.y );
516             output.emplace_back( std::make_pair( fheroes2::Rect( imageOffset.x, imageOffset.y, sprite.width(), sprite.height() ), modeToSpell( spellIt->mode ) ) );
517 
518             if ( spellIt->duration > 0 ) {
519                 text.Set( std::to_string( spellIt->duration ), Font::SMALL );
520                 text.Blit( ow - text.w(), dst.y + maxSpriteHeight - text.h() + 1 );
521             }
522             ow -= sprite.width() + space;
523         }
524     }
525 
526     return output;
527 }
528 
DrawMonsterInfo(const fheroes2::Point & offset,const Troop & troop)529 void DrawMonsterInfo( const fheroes2::Point & offset, const Troop & troop )
530 {
531     // name
532     Text text( troop.GetName(), Font::YELLOW_BIG );
533     fheroes2::Point pos( offset.x + 140 - text.w() / 2, offset.y + 40 );
534     text.Blit( pos.x, pos.y );
535 
536     // Description.
537     const std::vector<std::string> descriptions = fheroes2::getMonsterPropertiesDescription( troop.GetID() );
538     if ( !descriptions.empty() ) {
539         const int32_t descriptionWidth = 210;
540         const int32_t maximumRowCount = 3;
541         const int32_t rowHeight = fheroes2::Text( std::string(), { fheroes2::FontSize::SMALL, fheroes2::FontColor::WHITE } ).height();
542 
543         bool asSolidText = true;
544         if ( descriptions.size() <= static_cast<size_t>( maximumRowCount ) ) {
545             asSolidText = false;
546             for ( const std::string & sentence : descriptions ) {
547                 if ( fheroes2::Text( sentence, { fheroes2::FontSize::SMALL, fheroes2::FontColor::WHITE } ).width() > descriptionWidth ) {
548                     asSolidText = true;
549                     break;
550                 }
551             }
552         }
553 
554         if ( asSolidText ) {
555             std::string description;
556             for ( const std::string & sentence : descriptions ) {
557                 if ( !description.empty() ) {
558                     description += ' ';
559                 }
560 
561                 description += sentence;
562             }
563 
564             const fheroes2::Text descriptionText( description, { fheroes2::FontSize::SMALL, fheroes2::FontColor::WHITE } );
565             const int32_t rowCount = descriptionText.rows( descriptionWidth );
566 
567             descriptionText.draw( offset.x + 37, offset.y + 185 + ( maximumRowCount - rowCount ) * rowHeight, descriptionWidth, fheroes2::Display::instance() );
568         }
569         else {
570             int32_t sentenceId = maximumRowCount - static_cast<int32_t>( descriptions.size() ); // safe to cast as we check the size before.
571             for ( const std::string & sentence : descriptions ) {
572                 const fheroes2::Text descriptionText( sentence, { fheroes2::FontSize::SMALL, fheroes2::FontColor::WHITE } );
573 
574                 descriptionText.draw( offset.x + 37, offset.y + 185 + sentenceId * rowHeight, descriptionWidth, fheroes2::Display::instance() );
575                 ++sentenceId;
576             }
577         }
578     }
579 
580     // amount
581     text.Set( std::to_string( troop.GetCount() ), Font::BIG );
582     pos.x = offset.x + offsetXAmountBox + widthAmountBox / 2 - text.w() / 2;
583     pos.y = offset.y + offsetYAmountBox + heightAmountBox / 2 - text.h() / 2;
584     text.Blit( pos.x, pos.y );
585 }
586 
DrawMonster(fheroes2::RandomMonsterAnimation & monsterAnimation,const Troop & troop,const fheroes2::Point & offset,bool isReflected,bool isAnimated,const fheroes2::Rect & roi)587 void DrawMonster( fheroes2::RandomMonsterAnimation & monsterAnimation, const Troop & troop, const fheroes2::Point & offset, bool isReflected, bool isAnimated,
588                   const fheroes2::Rect & roi )
589 {
590     const fheroes2::Sprite & monsterSprite = fheroes2::AGG::GetICN( monsterAnimation.icnFile(), monsterAnimation.frameId() );
591     fheroes2::Point monsterPos( offset.x, offset.y + monsterSprite.y() );
592     if ( isReflected )
593         monsterPos.x -= monsterSprite.x() - ( troop.isWide() ? CELLW / 2 : 0 ) - monsterAnimation.offset() + monsterSprite.width();
594     else
595         monsterPos.x += monsterSprite.x() - ( troop.isWide() ? CELLW / 2 : 0 ) - monsterAnimation.offset();
596 
597     fheroes2::Point inPos( 0, 0 );
598     fheroes2::Point outPos( monsterPos.x, monsterPos.y );
599     fheroes2::Size inSize( monsterSprite.width(), monsterSprite.height() );
600 
601     fheroes2::Display & display = fheroes2::Display::instance();
602 
603     if ( fheroes2::FitToRoi( monsterSprite, inPos, display, outPos, inSize, roi ) ) {
604         fheroes2::Blit( monsterSprite, inPos, display, outPos, inSize, isReflected );
605     }
606 
607     if ( isAnimated )
608         monsterAnimation.increment();
609 }
610 
ArmyJoinFree(const Troop & troop,Heroes & hero)611 int Dialog::ArmyJoinFree( const Troop & troop, Heroes & hero )
612 {
613     fheroes2::Display & display = fheroes2::Display::instance();
614     const bool isEvilInterface = Settings::Get().ExtGameEvilInterface();
615 
616     // setup cursor
617     const CursorRestorer cursorRestorer( true, Cursor::POINTER );
618 
619     const Text title( _( "Followers" ), Font::YELLOW_BIG );
620 
621     std::string message = _( "A group of %{monster} with a desire for greater glory wish to join you.\nDo you accept?" );
622     StringReplace( message, "%{monster}", StringLower( troop.GetMultiName() ) );
623 
624     TextBox textbox( message, Font::BIG, BOXAREA_WIDTH );
625     const int buttons = Dialog::YES | Dialog::NO;
626     int posy = 0;
627 
628     FrameBox box( 10 + 2 * title.h() + textbox.h() + 10, true );
629     const fheroes2::Rect & pos = box.GetArea();
630 
631     title.Blit( pos.x + ( pos.width - title.w() ) / 2, pos.y );
632 
633     posy = pos.y + 2 * title.h() - 3;
634     textbox.Blit( pos.x, posy );
635 
636     fheroes2::ButtonGroup btnGroup( pos, buttons );
637 
638     const int armyButtonIcn = isEvilInterface ? ICN::EVIL_ARMY_BUTTON : ICN::GOOD_ARMY_BUTTON;
639     const fheroes2::Sprite & armyButtonReleased = fheroes2::AGG::GetICN( armyButtonIcn, 0 );
640     const fheroes2::Sprite & armyButtonPressed = fheroes2::AGG::GetICN( armyButtonIcn, 1 );
641 
642     fheroes2::ButtonSprite btnHeroes = fheroes2::makeButtonWithBackground( pos.x + pos.width / 2 - armyButtonReleased.width() / 2, pos.y + pos.height - 35,
643                                                                            armyButtonReleased, armyButtonPressed, display );
644 
645     if ( hero.GetArmy().GetCount() < hero.GetArmy().Size() || hero.GetArmy().HasMonster( troop ) )
646         btnHeroes.disable();
647     else {
648         // TextBox textbox2(_("Not room in\nthe garrison"), Font::SMALL, 100);
649         // textbox2.Blit(btnHeroes.x - 35, btnHeroes.y - 30);
650         btnHeroes.draw();
651         btnGroup.button( 0 ).disable();
652     }
653 
654     btnGroup.draw();
655     display.render();
656 
657     LocalEvent & le = LocalEvent::Get();
658 
659     // message loop
660     int result = Dialog::ZERO;
661 
662     while ( result == Dialog::ZERO && le.HandleEvents() ) {
663         if ( btnHeroes.isEnabled() )
664             le.MousePressLeft( btnHeroes.area() ) ? btnHeroes.drawOnPress() : btnHeroes.drawOnRelease();
665 
666         result = btnGroup.processEvents();
667 
668         if ( btnHeroes.isEnabled() && le.MouseClickLeft( btnHeroes.area() ) ) {
669             LocalEvent::GetClean();
670             hero.OpenDialog( false, false, true, true );
671 
672             if ( hero.GetArmy().GetCount() < hero.GetArmy().Size() ) {
673                 btnGroup.button( 0 ).enable();
674             }
675             else {
676                 btnGroup.button( 0 ).disable();
677             }
678 
679             btnGroup.draw();
680 
681             display.render();
682         }
683         else if ( le.MousePressRight( btnHeroes.area() ) ) {
684             Dialog::Message( "", _( "View Hero" ), Font::BIG );
685         }
686     }
687 
688     return result;
689 }
690 
ArmyJoinWithCost(const Troop & troop,u32 join,u32 gold,Heroes & hero)691 int Dialog::ArmyJoinWithCost( const Troop & troop, u32 join, u32 gold, Heroes & hero )
692 {
693     fheroes2::Display & display = fheroes2::Display::instance();
694     const bool isEvilInterface = Settings::Get().ExtGameEvilInterface();
695 
696     // setup cursor
697     const CursorRestorer cursorRestorer( true, Cursor::POINTER );
698 
699     std::string message;
700 
701     if ( troop.GetCount() == 1 ) {
702         message = _( "The %{monster} is swayed by your diplomatic tongue, and offers to join your army for the sum of %{gold} gold.\nDo you accept?" );
703     }
704     else {
705         message = _( "The creatures are swayed by your diplomatic\ntongue, and make you an offer:\n \n" );
706 
707         if ( join != troop.GetCount() )
708             message += _( "%{offer} of the %{total} %{monster} will join your army, and the rest will leave you alone, for the sum of %{gold} gold.\nDo you accept?" );
709         else
710             message += _( "All %{offer} of the %{monster} will join your army for the sum of %{gold} gold.\nDo you accept?" );
711     }
712 
713     StringReplace( message, "%{offer}", join );
714     StringReplace( message, "%{total}", troop.GetCount() );
715     StringReplace( message, "%{monster}", StringLower( troop.GetPluralName( join ) ) );
716     StringReplace( message, "%{gold}", gold );
717 
718     TextBox textbox( message, Font::BIG, BOXAREA_WIDTH );
719     const int buttons = Dialog::YES | Dialog::NO;
720     const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::RESOURCE, 6 );
721     int posy = 0;
722     Text text;
723 
724     message = _( "(Rate: %{percent})" );
725     StringReplace( message, "%{percent}", troop.GetMonster().GetCost().gold * join * 100 / gold );
726     text.Set( message, Font::BIG );
727 
728     FrameBox box( 10 + textbox.h() + 10 + text.h() + 40 + sprite.height() + 10, true );
729     const fheroes2::Rect & pos = box.GetArea();
730 
731     posy = pos.y + 10;
732     textbox.Blit( pos.x, posy );
733 
734     posy += textbox.h() + 10;
735     text.Blit( pos.x + ( pos.width - text.w() ) / 2, posy );
736 
737     posy += text.h() + 40;
738     fheroes2::Blit( sprite, display, pos.x + ( pos.width - sprite.width() ) / 2, posy );
739 
740     TextSprite tsTotal( std::to_string( gold ) + " (" + _( "Total: " ) + std::to_string( world.GetKingdom( hero.GetColor() ).GetFunds().Get( Resource::GOLD ) ) + ")",
741                         Font::SMALL, pos.x + ( pos.width - text.w() ) / 2, posy + sprite.height() + 5 );
742     tsTotal.Show();
743 
744     fheroes2::ButtonGroup btnGroup( pos, buttons );
745 
746     const int icnMarket = isEvilInterface ? ICN::EVIL_MARKET_BUTTON : ICN::GOOD_MARKET_BUTTON;
747     const int icnHeroes = isEvilInterface ? ICN::EVIL_ARMY_BUTTON : ICN::GOOD_ARMY_BUTTON;
748 
749     fheroes2::ButtonSprite btnMarket = fheroes2::makeButtonWithBackground( pos.x + pos.width / 2 - 60 - 36, posy, fheroes2::AGG::GetICN( icnMarket, 0 ),
750                                                                            fheroes2::AGG::GetICN( icnMarket, 1 ), display );
751 
752     fheroes2::ButtonSprite btnHeroes
753         = fheroes2::makeButtonWithBackground( pos.x + pos.width / 2 + 60, posy, fheroes2::AGG::GetICN( icnHeroes, 0 ), fheroes2::AGG::GetICN( icnHeroes, 1 ), display );
754 
755     Kingdom & kingdom = hero.GetKingdom();
756 
757     fheroes2::Rect btnMarketArea = btnMarket.area();
758     fheroes2::Rect btnHeroesArea = btnHeroes.area();
759 
760     if ( !kingdom.AllowPayment( payment_t( Resource::GOLD, gold ) ) )
761         btnGroup.button( 0 ).disable();
762 
763     TextSprite tsNotEnoughGold;
764     tsNotEnoughGold.SetPos( btnMarketArea.x - 25, btnMarketArea.y - 17 );
765 
766     fheroes2::ImageRestorer marketButtonRestorer( display, btnMarket.area().x, btnMarket.area().y, btnMarket.area().width, btnMarket.area().height );
767 
768     if ( kingdom.AllowPayment( payment_t( Resource::GOLD, gold ) ) || kingdom.GetCountMarketplace() == 0 ) {
769         tsNotEnoughGold.Hide();
770         btnMarket.disable();
771         btnMarket.hide();
772     }
773     else {
774         std::string msg = _( "Not enough gold (%{gold})" );
775         StringReplace( msg, "%{gold}", gold - kingdom.GetFunds().Get( Resource::GOLD ) );
776         tsNotEnoughGold.SetText( msg, Font::SMALL );
777         tsNotEnoughGold.Show();
778         btnMarket.enable();
779         btnMarket.draw();
780     }
781 
782     TextSprite noRoom1;
783     noRoom1.SetText( _( "No room in" ), Font::SMALL );
784     noRoom1.SetPos( btnHeroesArea.x - 16, btnHeroesArea.y - 30 );
785     TextSprite noRoom2;
786     noRoom2.SetText( _( "the garrison" ), Font::SMALL );
787     noRoom2.SetPos( btnHeroesArea.x - 23, btnHeroesArea.y - 15 );
788 
789     if ( hero.GetArmy().GetCount() < hero.GetArmy().Size() || hero.GetArmy().HasMonster( troop ) )
790         btnHeroes.disable();
791     else {
792         noRoom1.Show();
793         noRoom2.Show();
794         btnHeroes.draw();
795         btnGroup.button( 0 ).disable();
796     }
797 
798     btnGroup.draw();
799     display.render();
800 
801     LocalEvent & le = LocalEvent::Get();
802 
803     // message loop
804     int result = Dialog::ZERO;
805 
806     while ( result == Dialog::ZERO && le.HandleEvents() ) {
807         if ( btnMarket.isEnabled() )
808             le.MousePressLeft( btnMarketArea ) ? btnMarket.drawOnPress() : btnMarket.drawOnRelease();
809 
810         if ( btnHeroes.isEnabled() )
811             le.MousePressLeft( btnHeroesArea ) ? btnHeroes.drawOnPress() : btnHeroes.drawOnRelease();
812 
813         result = btnGroup.processEvents();
814 
815         bool needRedraw = false;
816 
817         if ( btnMarket.isEnabled() && le.MouseClickLeft( btnMarketArea ) ) {
818             Marketplace( kingdom, false );
819 
820             needRedraw = true;
821         }
822         else if ( btnHeroes.isEnabled() && le.MouseClickLeft( btnHeroesArea ) ) {
823             LocalEvent::GetClean();
824             hero.OpenDialog( false, false, true, true );
825 
826             needRedraw = true;
827         }
828 
829         if ( !needRedraw ) {
830             continue;
831         }
832 
833         tsTotal.Hide();
834         tsTotal.SetText( std::to_string( gold ) + " (total: " + std::to_string( world.GetKingdom( hero.GetColor() ).GetFunds().Get( Resource::GOLD ) ) + ")" );
835         tsTotal.Show();
836 
837         const bool allowPayment = kingdom.AllowPayment( payment_t( Resource::GOLD, gold ) );
838         const bool enoughRoom = hero.GetArmy().GetCount() < hero.GetArmy().Size() || hero.GetArmy().HasMonster( troop );
839 
840         if ( allowPayment && enoughRoom ) {
841             btnGroup.button( 0 ).enable();
842         }
843         else {
844             btnGroup.button( 0 ).disable();
845         }
846 
847         btnGroup.draw();
848 
849         if ( allowPayment || kingdom.GetCountMarketplace() == 0 ) {
850             tsNotEnoughGold.Hide();
851             btnMarket.disable();
852             btnMarket.hide();
853             marketButtonRestorer.restore();
854         }
855         else {
856             std::string msg = _( "Not enough gold (%{gold})" );
857             StringReplace( msg, "%{gold}", gold - kingdom.GetFunds().Get( Resource::GOLD ) );
858             tsNotEnoughGold.SetText( msg, Font::SMALL );
859             tsNotEnoughGold.Show();
860             btnMarket.enable();
861             btnMarket.show();
862         }
863 
864         btnMarket.draw();
865 
866         if ( enoughRoom ) {
867             noRoom1.Hide();
868             noRoom2.Hide();
869         }
870         else {
871             noRoom1.Show();
872             noRoom2.Show();
873         }
874 
875         display.render();
876     }
877 
878     return result;
879 }
880