1 /***************************************************************************
2 * Copyright (C) 2010 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 <algorithm>
24 #include <cstdlib>
25
26 #include "agg.h"
27 #include "agg_image.h"
28 #include "audio.h"
29 #include "battle_arena.h"
30 #include "battle_army.h"
31 #include "battle_bridge.h"
32 #include "battle_catapult.h"
33 #include "battle_cell.h"
34 #include "battle_command.h"
35 #include "battle_interface.h"
36 #include "battle_tower.h"
37 #include "battle_troop.h"
38 #include "castle.h"
39 #include "game.h"
40 #include "game_delays.h"
41 #include "ground.h"
42 #include "icn.h"
43 #include "interface_list.h"
44 #include "logging.h"
45 #include "monster_anim.h"
46 #include "mus.h"
47 #include "pal.h"
48 #include "race.h"
49 #include "rand.h"
50 #include "settings.h"
51 #include "tools.h"
52 #include "translations.h"
53 #include "ui_window.h"
54 #include "world.h"
55
56 #include <cassert>
57
58 #define ARMYORDERW 40
59
60 namespace
61 {
62 const int32_t cellYOffset = -9;
63
64 struct LightningPoint
65 {
LightningPoint__anondf995ea00111::LightningPoint66 explicit LightningPoint( const fheroes2::Point & p = fheroes2::Point(), uint32_t thick = 1 )
67 : point( p )
68 , thickness( thick )
69 {}
70
71 fheroes2::Point point;
72 uint32_t thickness;
73 };
74
getDistance(const fheroes2::Point & p1,const fheroes2::Point & p2)75 double getDistance( const fheroes2::Point & p1, const fheroes2::Point & p2 )
76 {
77 const double diffX = p1.x - p2.x;
78 const double diffY = p1.y - p2.y;
79
80 return std::sqrt( diffX * diffX + diffY * diffY );
81 }
82
rotate(const fheroes2::Point & point,double angle)83 fheroes2::Point rotate( const fheroes2::Point & point, double angle )
84 {
85 const double sinValue = sin( angle );
86 const double cosValue = cos( angle );
87
88 return fheroes2::Point( static_cast<int32_t>( point.x * cosValue - point.y * sinValue ), static_cast<int32_t>( point.x * sinValue + point.y * cosValue ) );
89 }
90
getAngle(const fheroes2::Point & start,const fheroes2::Point & end)91 double getAngle( const fheroes2::Point & start, const fheroes2::Point & end )
92 {
93 return std::atan2( end.y - start.y, end.x - start.x );
94 }
95
GenerateLightning(const fheroes2::Point & src,const fheroes2::Point & dst)96 std::vector<std::pair<LightningPoint, LightningPoint> > GenerateLightning( const fheroes2::Point & src, const fheroes2::Point & dst )
97 {
98 const int distance = static_cast<int>( getDistance( src, dst ) );
99 const double angle = getAngle( src, dst );
100
101 int iterationCount = ( distance + 50 ) / 100;
102 if ( iterationCount < 3 )
103 iterationCount = 3;
104 if ( iterationCount > 5 )
105 iterationCount = 5;
106
107 std::vector<std::pair<LightningPoint, LightningPoint> > lines;
108 lines.emplace_back( LightningPoint( fheroes2::Point( 0, 0 ), 5 ), LightningPoint( fheroes2::Point( distance, 0 ), 3 ) );
109
110 int maxOffset = distance;
111
112 for ( int step = 0; step < iterationCount; ++step ) {
113 std::vector<std::pair<LightningPoint, LightningPoint> > oldLines;
114 std::swap( lines, oldLines );
115
116 for ( size_t i = 0; i < oldLines.size(); ++i ) {
117 fheroes2::Point middle( oldLines[i].first.point + oldLines[i].second.point );
118 middle.x /= 2;
119 middle.y /= 2;
120
121 const bool isPositive = ( Rand::Get( 1, 2 ) == 1 );
122 int offsetY = static_cast<int>( Rand::Get( 1, 10 ) ) * maxOffset / 100;
123 if ( offsetY < 1 )
124 offsetY = 1;
125
126 middle.y += isPositive ? offsetY : -offsetY;
127
128 const uint32_t middleThickness = ( oldLines[i].first.thickness + oldLines[i].second.thickness ) / 2;
129
130 const LightningPoint middlePoint( middle, middleThickness );
131
132 lines.emplace_back( oldLines[i].first, middlePoint );
133 lines.emplace_back( middlePoint, oldLines[i].second );
134
135 if ( Rand::Get( 1, 4 ) == 1 ) { // 25%
136 offsetY = static_cast<int>( Rand::Get( 1, 10 ) ) * maxOffset / 100;
137 const int32_t x = static_cast<int32_t>( ( middle.x - oldLines[i].first.point.x ) * 0.7 ) + middle.x;
138 const int32_t y = static_cast<int32_t>( ( middle.y - oldLines[i].first.point.y ) * 0.7 ) + middle.y + ( isPositive ? offsetY : -offsetY );
139 lines.emplace_back( middlePoint, LightningPoint( fheroes2::Point( x, y ), 1 ) );
140 }
141 }
142
143 maxOffset /= 2;
144 }
145
146 for ( size_t i = 0; i < lines.size(); ++i ) {
147 lines[i].first.point = rotate( lines[i].first.point, angle ) + src;
148 lines[i].second.point = rotate( lines[i].second.point, angle ) + src;
149 }
150
151 return lines;
152 }
153
RedrawLightning(const std::vector<std::pair<LightningPoint,LightningPoint>> & lightning,uint8_t color,fheroes2::Image & surface,const fheroes2::Rect & roi=fheroes2::Rect ())154 void RedrawLightning( const std::vector<std::pair<LightningPoint, LightningPoint> > & lightning, uint8_t color, fheroes2::Image & surface,
155 const fheroes2::Rect & roi = fheroes2::Rect() )
156 {
157 for ( size_t i = 0; i < lightning.size(); ++i ) {
158 const fheroes2::Point & first = lightning[i].first.point;
159 const fheroes2::Point & second = lightning[i].second.point;
160 const bool isHorizontal = std::abs( first.x - second.x ) >= std::abs( first.y - second.y );
161 const int xOffset = isHorizontal ? 0 : 1;
162 const int yOffset = isHorizontal ? 1 : 0;
163
164 fheroes2::DrawLine( surface, first, second, color, roi );
165
166 for ( uint32_t thickness = 1; thickness < lightning[i].second.thickness; ++thickness ) {
167 const bool isUpper = ( ( thickness % 2 ) == 1 );
168 const int offset = isUpper ? ( thickness + 1 ) / 2 : -static_cast<int>( ( thickness + 1 ) / 2 );
169 const int x = xOffset * offset;
170 const int y = yOffset * offset;
171
172 fheroes2::DrawLine( surface, fheroes2::Point( first.x + x, first.y + y ), fheroes2::Point( second.x + x, second.y + y ), color, roi );
173 }
174
175 for ( uint32_t thickness = lightning[i].second.thickness; thickness < lightning[i].first.thickness; ++thickness ) {
176 const bool isUpper = ( ( thickness % 2 ) == 1 );
177 const int offset = isUpper ? ( thickness + 1 ) / 2 : -static_cast<int>( ( thickness + 1 ) / 2 );
178
179 fheroes2::DrawLine( surface, fheroes2::Point( first.x + xOffset * offset, first.y + yOffset * offset ), second, color, roi );
180 }
181 }
182 }
183 }
184
185 namespace Battle
186 {
187 int GetIndexIndicator( const Unit & );
188 int GetSwordCursorDirection( int );
189 int GetDirectionFromCursorSword( u32 );
190 int GetCursorFromSpell( int );
191
192 struct CursorPosition
193 {
CursorPositionBattle::CursorPosition194 CursorPosition()
195 : index( -1 )
196 {}
197
198 fheroes2::Point coord;
199 int32_t index;
200 };
201
202 class StatusListBox : public ::Interface::ListBox<std::string>
203 {
204 public:
StatusListBox()205 StatusListBox()
206 : openlog( false )
207 {}
208
SetPosition(u32 px,u32 py)209 void SetPosition( u32 px, u32 py )
210 {
211 const uint32_t mx = 6;
212 const uint32_t sw = fheroes2::Display::DEFAULT_WIDTH;
213 const uint32_t sh = mx * 20;
214 border.SetPosition( px, py - sh - 2, sw - 32, sh - 30 );
215 const fheroes2::Rect & area = border.GetArea();
216 const int32_t ax = area.x + area.width - 20;
217
218 SetTopLeft( area.getPosition() );
219 SetAreaMaxItems( mx );
220
221 SetScrollButtonUp( ICN::DROPLISL, 6, 7, fheroes2::Point( ax + 8, area.y - 10 ) );
222 SetScrollButtonDn( ICN::DROPLISL, 8, 9, fheroes2::Point( ax + 8, area.y + area.height - 11 ) );
223 SetScrollBar( fheroes2::AGG::GetICN( ICN::DROPLISL, 13 ), fheroes2::Rect( ax + 5 + 8, buttonPgUp.area().y + buttonPgUp.area().height + 3, 12,
224 buttonPgDn.area().y - ( buttonPgUp.area().y + buttonPgUp.area().height ) - 7 ) );
225 _scrollbar.hide();
226 SetAreaItems( fheroes2::Rect( area.x, area.y, area.width - 10, area.height ) );
227 SetListContent( messages );
228 _scrollbar.show();
229 }
230
GetArea() const231 const fheroes2::Rect & GetArea() const
232 {
233 return border.GetRect();
234 }
235
AddMessage(const std::string & str)236 void AddMessage( const std::string & str )
237 {
238 messages.push_back( str );
239 SetListContent( messages );
240 SetCurrent( messages.size() - 1 );
241 if ( !openlog ) {
242 _scrollbar.hide();
243 }
244 }
245
RedrawItem(const std::string & str,int32_t px,int32_t py,bool)246 void RedrawItem( const std::string & str, int32_t px, int32_t py, bool ) override
247 {
248 const Text text( str, Font::BIG );
249 text.Blit( px, py );
250 }
251
RedrawBackground(const fheroes2::Point & pt)252 void RedrawBackground( const fheroes2::Point & pt ) override
253 {
254 (void)pt;
255
256 fheroes2::Display & display = fheroes2::Display::instance();
257 const fheroes2::Sprite & sp1 = fheroes2::AGG::GetICN( ICN::DROPLISL, 10 );
258 const fheroes2::Sprite & sp2 = fheroes2::AGG::GetICN( ICN::DROPLISL, 12 );
259 const fheroes2::Sprite & sp3 = fheroes2::AGG::GetICN( ICN::DROPLISL, 11 );
260 const int32_t ax = buttonPgUp.area().x;
261 const int32_t ah = buttonPgDn.area().y - ( buttonPgUp.area().y + buttonPgUp.area().height );
262
263 const fheroes2::Rect & borderRect = border.GetRect();
264 Dialog::FrameBorder::RenderOther( fheroes2::AGG::GetICN( ICN::TEXTBAK2, 0 ), borderRect );
265
266 for ( int32_t i = 0; i < ( ah / sp3.height() ); ++i )
267 fheroes2::Blit( sp3, display, ax, buttonPgUp.area().y + buttonPgUp.area().height + ( sp3.height() * i ) );
268
269 fheroes2::Blit( sp1, display, ax, buttonPgUp.area().y + buttonPgUp.area().height );
270 fheroes2::Blit( sp2, display, ax, buttonPgDn.area().y - sp2.height() );
271 }
272
ActionCurrentUp()273 void ActionCurrentUp() override
274 {
275 // Do nothing.
276 }
277
ActionCurrentDn()278 void ActionCurrentDn() override
279 {
280 // Do nothing.
281 }
282
ActionListDoubleClick(std::string &)283 void ActionListDoubleClick( std::string & ) override
284 {
285 // Do nothing.
286 }
287
ActionListSingleClick(std::string &)288 void ActionListSingleClick( std::string & ) override
289 {
290 // Do nothing.
291 }
292
ActionListPressRight(std::string &)293 void ActionListPressRight( std::string & ) override
294 {
295 // Do nothing.
296 }
297
SetOpenLog(const bool f)298 void SetOpenLog( const bool f )
299 {
300 openlog = f;
301 }
302
isOpenLog() const303 bool isOpenLog() const
304 {
305 return openlog;
306 }
307
308 private:
309 Dialog::FrameBorder border;
310 std::vector<std::string> messages;
311 bool openlog;
312 };
313
matchHeroType(const HeroBase * base)314 int matchHeroType( const HeroBase * base )
315 {
316 if ( base->isCaptain() )
317 return CAPTAIN;
318
319 switch ( base->GetRace() ) {
320 case Race::BARB:
321 return BARBARIAN;
322 case Race::SORC:
323 return SORCERESS;
324 case Race::WRLK:
325 return WARLOCK;
326 case Race::WZRD:
327 return WIZARD;
328 case Race::NECR:
329 return NECROMANCER;
330 default:
331 break;
332 }
333 return KNIGHT;
334 }
335
getHeroAnimation(const HeroBase * hero,int animation)336 const std::vector<int> & getHeroAnimation( const HeroBase * hero, int animation )
337 {
338 static std::vector<int> staticAnim;
339 if ( staticAnim.empty() ) {
340 staticAnim.push_back( 1 );
341 }
342
343 if ( !hero || animation == OP_STATIC )
344 return staticAnim;
345
346 const int heroType = matchHeroType( hero );
347
348 if ( animation == OP_SORROW ) {
349 static const std::vector<int> sorrowAnim = { 2, 3, 4, 5, 4, 5, 4, 3, 2 };
350
351 return ( heroType == CAPTAIN ) ? staticAnim : sorrowAnim;
352 }
353
354 static std::vector<int> heroTypeAnim[7][9];
355
356 if ( heroTypeAnim[heroType][animation].empty() ) {
357 const int sourceArray[7][9][9] = {
358 // JOY CAST_MASS CAST_UP CAST_DOWN IDLE
359 {{6, 7, 8, 9, 8, 9, 8, 7, 6}, {10, 11}, {10}, {6, 12, 13}, {12, 6}, {2, 14}, {2}, {15, 16, 17}, {18, 19}}, // KNIGHT
360 {{6, 7, 8, 9, 9, 8, 7, 6}, {6, 10, 11}, {10, 6}, {6, 12, 13}, {12, 6}, {6, 14}, {6}, {15, 16, 17}, {18}}, // BARBARIAN
361 {{6, 7, 8, 7, 6}, {6, 7, 9}, {7, 6}, {6, 10, 11}, {10, 6}, {6, 12}, {6}, {13, 14, 15}, {16}}, // SORCERESS
362 {{6, 7, 8, 9, 10, 9, 8, 7, 6}, {6, 7, 11, 12}, {11, 6}, {6, 7, 13}, {6}, {6, 14}, {6}, {15, 16}, {6}}, // WARLOCK
363 {{6, 7, 8, 9, 8, 7, 6}, {6, 10, 11, 12, 13}, {12, 11, 10, 6}, {6, 14}, {6}, {6, 15}, {6}, {16, 17}, {18}}, // WIZARD
364 {{6, 7, 6, 7, 6, 7}, {7, 8, 9, 10, 11}, {10, 9, 7}, {7, 12, 13, 14, 15}, {7}, {7, 12, 13, 14, 16}, {7}, {17}, {18, 19}}, // NECROMANCER
365 {{1}, {2, 3, 4}, {3, 2}, {5, 6}, {5}, {5, 7}, {5}, {8, 9}, {10}} // CAPTAIN
366 };
367
368 for ( int frame = 0; frame < 9; ++frame ) {
369 if ( sourceArray[heroType][animation][frame] != 0 ) {
370 heroTypeAnim[heroType][animation].push_back( sourceArray[heroType][animation][frame] );
371 }
372 }
373 }
374
375 return heroTypeAnim[heroType][animation];
376 }
377 }
378
CursorAttack(u32 theme)379 bool CursorAttack( u32 theme )
380 {
381 switch ( theme ) {
382 case Cursor::WAR_ARROW:
383 case Cursor::WAR_BROKENARROW:
384 case Cursor::SWORD_TOPRIGHT:
385 case Cursor::SWORD_RIGHT:
386 case Cursor::SWORD_BOTTOMRIGHT:
387 case Cursor::SWORD_BOTTOMLEFT:
388 case Cursor::SWORD_LEFT:
389 case Cursor::SWORD_TOPLEFT:
390 case Cursor::SWORD_TOP:
391 case Cursor::SWORD_BOTTOM:
392 return true;
393 default:
394 break;
395 }
396
397 return false;
398 }
399
DrawHexagon(const uint8_t colorId)400 fheroes2::Image DrawHexagon( const uint8_t colorId )
401 {
402 const int r = 22;
403 const int l = 10;
404 const int w = CELLW;
405 const int h = CELLH;
406
407 fheroes2::Image sf( w + 1, h + 1 );
408 sf.reset();
409
410 fheroes2::DrawLine( sf, fheroes2::Point( r, 0 ), fheroes2::Point( 0, l ), colorId );
411 fheroes2::DrawLine( sf, fheroes2::Point( r, 0 ), fheroes2::Point( w, l ), colorId );
412
413 fheroes2::DrawLine( sf, fheroes2::Point( 0, l + 1 ), fheroes2::Point( 0, h - l ), colorId );
414 fheroes2::DrawLine( sf, fheroes2::Point( w, l + 1 ), fheroes2::Point( w, h - l ), colorId );
415
416 fheroes2::DrawLine( sf, fheroes2::Point( r, h ), fheroes2::Point( 0, h - l ), colorId );
417 fheroes2::DrawLine( sf, fheroes2::Point( r, h ), fheroes2::Point( w, h - l ), colorId );
418
419 return sf;
420 }
421
DrawHexagonShadow(const uint8_t alphaValue)422 fheroes2::Image DrawHexagonShadow( const uint8_t alphaValue )
423 {
424 const int l = 13;
425 const int w = CELLW;
426 const int h = CELLH;
427
428 fheroes2::Image sf( w, h );
429 sf.reset();
430 fheroes2::Rect rt( 0, l - 1, w + 1, 2 * l + 3 );
431 for ( int i = 0; i < w / 2; i += 2 ) {
432 --rt.y;
433 rt.height += 2;
434 rt.x += 2;
435 rt.width -= 4;
436 for ( int x = 0; x < rt.width; ++x ) {
437 for ( int y = 0; y < rt.height; ++y ) {
438 fheroes2::SetTransformPixel( sf, rt.x + x, rt.y + y, alphaValue );
439 }
440 }
441 }
442
443 return sf;
444 }
445
isFinishAnimFrame(const TargetInfo & info)446 bool Battle::TargetInfo::isFinishAnimFrame( const TargetInfo & info )
447 {
448 return info.defender && info.defender->isFinishAnimFrame();
449 }
450
GetCursorFromSpell(int spell)451 int Battle::GetCursorFromSpell( int spell )
452 {
453 switch ( spell ) {
454 case Spell::SLOW:
455 return Cursor::SP_SLOW;
456 case Spell::CURSE:
457 return Cursor::SP_CURSE;
458 case Spell::CURE:
459 return Cursor::SP_CURE;
460 case Spell::BLESS:
461 return Cursor::SP_BLESS;
462 case Spell::FIREBALL:
463 return Cursor::SP_FIREBALL;
464 case Spell::FIREBLAST:
465 return Cursor::SP_FIREBLAST;
466 case Spell::TELEPORT:
467 return Cursor::SP_TELEPORT;
468 case Spell::RESURRECT:
469 return Cursor::SP_RESURRECT;
470 case Spell::HASTE:
471 return Cursor::SP_HASTE;
472 case Spell::SHIELD:
473 return Cursor::SP_SHIELD;
474 case Spell::ARMAGEDDON:
475 return Cursor::SP_ARMAGEDDON;
476 case Spell::ANTIMAGIC:
477 return Cursor::SP_ANTIMAGIC;
478 case Spell::BERSERKER:
479 return Cursor::SP_BERSERKER;
480 case Spell::PARALYZE:
481 return Cursor::SP_PARALYZE;
482 case Spell::BLIND:
483 return Cursor::SP_BLIND;
484
485 case Spell::LIGHTNINGBOLT:
486 return Cursor::SP_LIGHTNINGBOLT;
487 case Spell::CHAINLIGHTNING:
488 return Cursor::SP_CHAINLIGHTNING;
489 case Spell::ELEMENTALSTORM:
490 return Cursor::SP_ELEMENTALSTORM;
491 case Spell::RESURRECTTRUE:
492 return Cursor::SP_RESURRECTTRUE;
493 case Spell::DISPEL:
494 return Cursor::SP_DISPEL;
495 case Spell::HOLYWORD:
496 return Cursor::SP_HOLYWORD;
497 case Spell::HOLYSHOUT:
498 return Cursor::SP_HOLYSHOUT;
499 case Spell::METEORSHOWER:
500 return Cursor::SP_METEORSHOWER;
501
502 case Spell::ANIMATEDEAD:
503 return Cursor::SP_ANIMATEDEAD;
504 case Spell::MIRRORIMAGE:
505 return Cursor::SP_MIRRORIMAGE;
506 case Spell::BLOODLUST:
507 return Cursor::SP_BLOODLUST;
508 case Spell::DEATHRIPPLE:
509 return Cursor::SP_DEATHRIPPLE;
510 case Spell::DEATHWAVE:
511 return Cursor::SP_DEATHWAVE;
512 case Spell::STEELSKIN:
513 return Cursor::SP_STEELSKIN;
514 case Spell::STONESKIN:
515 return Cursor::SP_STONESKIN;
516 case Spell::DRAGONSLAYER:
517 return Cursor::SP_DRAGONSLAYER;
518 case Spell::EARTHQUAKE:
519 return Cursor::SP_EARTHQUAKE;
520 case Spell::DISRUPTINGRAY:
521 return Cursor::SP_DISRUPTINGRAY;
522 case Spell::COLDRING:
523 return Cursor::SP_COLDRING;
524 case Spell::COLDRAY:
525 return Cursor::SP_COLDRAY;
526 case Spell::HYPNOTIZE:
527 return Cursor::SP_HYPNOTIZE;
528 case Spell::ARROW:
529 return Cursor::SP_ARROW;
530
531 default:
532 break;
533 }
534 return Cursor::WAR_NONE;
535 }
536
GetSwordCursorDirection(int dir)537 int Battle::GetSwordCursorDirection( int dir )
538 {
539 switch ( dir ) {
540 case BOTTOM_RIGHT:
541 return Cursor::SWORD_TOPLEFT;
542 case BOTTOM_LEFT:
543 return Cursor::SWORD_TOPRIGHT;
544 case RIGHT:
545 return Cursor::SWORD_LEFT;
546 case TOP_RIGHT:
547 return Cursor::SWORD_BOTTOMLEFT;
548 case TOP_LEFT:
549 return Cursor::SWORD_BOTTOMRIGHT;
550 case LEFT:
551 return Cursor::SWORD_RIGHT;
552 default:
553 break;
554 }
555 return 0;
556 }
557
GetDirectionFromCursorSword(u32 sword)558 int Battle::GetDirectionFromCursorSword( u32 sword )
559 {
560 switch ( sword ) {
561 case Cursor::SWORD_TOPLEFT:
562 return BOTTOM_RIGHT;
563 case Cursor::SWORD_TOPRIGHT:
564 return BOTTOM_LEFT;
565 case Cursor::SWORD_LEFT:
566 return RIGHT;
567 case Cursor::SWORD_BOTTOMLEFT:
568 return TOP_RIGHT;
569 case Cursor::SWORD_BOTTOMRIGHT:
570 return TOP_LEFT;
571 case Cursor::SWORD_RIGHT:
572 return LEFT;
573 default:
574 break;
575 }
576
577 return UNKNOWN;
578 }
579
OpponentSprite(const fheroes2::Rect & area,const HeroBase * b,bool r)580 Battle::OpponentSprite::OpponentSprite( const fheroes2::Rect & area, const HeroBase * b, bool r )
581 : base( b )
582 , _currentAnim( getHeroAnimation( b, OP_STATIC ) )
583 , _animationType( OP_STATIC )
584 , _idleTimer( 8000 )
585 , _heroIcnId( ICN::UNKNOWN )
586 , reflect( r )
587 , _offset( area.x, area.y )
588 {
589 const bool isCaptain = b->isCaptain();
590 switch ( b->GetRace() ) {
591 case Race::KNGT:
592 _heroIcnId = isCaptain ? ICN::CMBTCAPK : ICN::CMBTHROK;
593 break;
594 case Race::BARB:
595 _heroIcnId = isCaptain ? ICN::CMBTCAPB : ICN::CMBTHROB;
596 break;
597 case Race::SORC:
598 _heroIcnId = isCaptain ? ICN::CMBTCAPS : ICN::CMBTHROS;
599 break;
600 case Race::WRLK:
601 _heroIcnId = isCaptain ? ICN::CMBTCAPW : ICN::CMBTHROW;
602 break;
603 case Race::WZRD:
604 _heroIcnId = isCaptain ? ICN::CMBTCAPZ : ICN::CMBTHROZ;
605 break;
606 case Race::NECR:
607 _heroIcnId = isCaptain ? ICN::CMBTCAPN : ICN::CMBTHRON;
608 break;
609 default:
610 // Did you add a new faction? Add the logic here.
611 assert( 0 );
612 break;
613 }
614
615 const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( _heroIcnId, _currentAnim.getFrame() );
616
617 if ( reflect ) {
618 pos.x = _offset.x + fheroes2::Display::DEFAULT_WIDTH - HERO_X_OFFSET - ( sprite.x() + sprite.width() );
619 pos.y = _offset.y + RIGHT_HERO_Y_OFFSET + sprite.y();
620 }
621 else {
622 pos.x = _offset.x + HERO_X_OFFSET + sprite.x();
623 pos.y = _offset.y + LEFT_HERO_Y_OFFSET + sprite.y();
624 }
625
626 if ( isCaptain ) {
627 if ( reflect )
628 pos.x += CAPTAIN_X_OFFSET;
629 else
630 pos.x -= CAPTAIN_X_OFFSET;
631 pos.y += CAPTAIN_Y_OFFSET;
632 }
633
634 pos.width = sprite.width();
635 pos.height = sprite.height();
636 }
637
GetHero(void) const638 const HeroBase * Battle::OpponentSprite::GetHero( void ) const
639 {
640 return base;
641 }
642
Offset() const643 fheroes2::Point Battle::OpponentSprite::Offset() const
644 {
645 return _offset;
646 }
647
IncreaseAnimFrame(bool loop)648 void Battle::OpponentSprite::IncreaseAnimFrame( bool loop )
649 {
650 _currentAnim.playAnimation( loop );
651 }
652
SetAnimation(int rule)653 void Battle::OpponentSprite::SetAnimation( int rule )
654 {
655 _animationType = rule;
656 _currentAnim = getHeroAnimation( base, rule );
657 }
658
isFinishFrame(void) const659 bool Battle::OpponentSprite::isFinishFrame( void ) const
660 {
661 return _currentAnim.isLastFrame();
662 }
663
GetArea(void) const664 const fheroes2::Rect & Battle::OpponentSprite::GetArea( void ) const
665 {
666 return pos;
667 }
668
GetCastPosition(void) const669 fheroes2::Point Battle::OpponentSprite::GetCastPosition( void ) const
670 {
671 const bool isCaptain = base->isCaptain();
672 fheroes2::Point offset;
673 switch ( base->GetRace() ) {
674 case Race::KNGT:
675 offset.x = isCaptain ? 0 : 13;
676 offset.y = isCaptain ? 3 : -7;
677 break;
678 case Race::BARB:
679 offset.x = isCaptain ? 0 : 16;
680 offset.y = isCaptain ? 3 : -15;
681 break;
682 case Race::SORC:
683 offset.x = isCaptain ? 0 : 11;
684 offset.y = isCaptain ? 3 : -8;
685 break;
686 case Race::WRLK:
687 offset.x = isCaptain ? 2 : 9;
688 offset.y = isCaptain ? 5 : -11;
689 break;
690 case Race::WZRD:
691 offset.x = isCaptain ? 5 : 1;
692 offset.y = isCaptain ? 8 : -9;
693 break;
694 case Race::NECR:
695 offset.x = isCaptain ? 5 : 13;
696 offset.y = isCaptain ? 6 : -7;
697 break;
698 default:
699 break;
700 }
701
702 return fheroes2::Point( pos.x + ( reflect ? offset.x : pos.width - offset.x ), pos.y + pos.height / 2 + offset.y );
703 }
704
Redraw(fheroes2::Image & dst) const705 void Battle::OpponentSprite::Redraw( fheroes2::Image & dst ) const
706 {
707 const fheroes2::Sprite & hero = fheroes2::AGG::GetICN( _heroIcnId, _currentAnim.getFrame() );
708
709 fheroes2::Point offset( _offset );
710 if ( base->isCaptain() ) {
711 if ( reflect )
712 offset.x += CAPTAIN_X_OFFSET;
713 else
714 offset.x -= CAPTAIN_X_OFFSET;
715 offset.y += CAPTAIN_Y_OFFSET;
716 }
717
718 if ( reflect )
719 fheroes2::Blit( hero, dst, offset.x + fheroes2::Display::DEFAULT_WIDTH - HERO_X_OFFSET - ( hero.x() + hero.width() ), offset.y + RIGHT_HERO_Y_OFFSET + hero.y(),
720 reflect );
721 else
722 fheroes2::Blit( hero, dst, offset.x + HERO_X_OFFSET + hero.x(), offset.y + LEFT_HERO_Y_OFFSET + hero.y() );
723 }
724
Update()725 void Battle::OpponentSprite::Update()
726 {
727 if ( _currentAnim.isLastFrame() ) {
728 if ( _animationType != OP_STATIC ) {
729 if ( _animationType != OP_CAST_MASS && _animationType != OP_CAST_UP && _animationType != OP_CAST_DOWN )
730 SetAnimation( OP_STATIC );
731 }
732 else if ( _idleTimer.checkDelay() ) {
733 SetAnimation( ( Rand::Get( 1, 3 ) < 2 ) ? OP_IDLE2 : OP_IDLE );
734 }
735 }
736 else {
737 _currentAnim.playAnimation();
738 }
739 }
740
Status()741 Battle::Status::Status()
742 : back1( fheroes2::AGG::GetICN( ICN::TEXTBAR, 8 ) )
743 , back2( fheroes2::AGG::GetICN( ICN::TEXTBAR, 9 ) )
744 , listlog( nullptr )
745 {
746 width = back1.width();
747 height = back1.height() + back2.height();
748
749 bar1.Set( Font::BIG );
750 bar2.Set( Font::BIG );
751 }
752
SetPosition(s32 cx,s32 cy)753 void Battle::Status::SetPosition( s32 cx, s32 cy )
754 {
755 fheroes2::Rect::x = cx;
756 fheroes2::Rect::y = cy;
757 }
758
SetMessage(const std::string & str,bool top)759 void Battle::Status::SetMessage( const std::string & str, bool top )
760 {
761 if ( top ) {
762 bar1.Set( str );
763 if ( listlog )
764 listlog->AddMessage( str );
765 }
766 else if ( str != message ) {
767 bar2.Set( str );
768 message = str;
769 }
770 }
771
Redraw(void) const772 void Battle::Status::Redraw( void ) const
773 {
774 fheroes2::Display & display = fheroes2::Display::instance();
775 fheroes2::Blit( back1, display, x, y );
776 fheroes2::Blit( back2, display, x, y + back1.height() );
777
778 if ( bar1.Size() )
779 bar1.Blit( x + ( back1.width() - bar1.w() ) / 2, y + 2 );
780 if ( bar2.Size() )
781 bar2.Blit( x + ( back2.width() - bar2.w() ) / 2, y + back1.height() - 2 );
782 }
783
GetMessage(void) const784 const std::string & Battle::Status::GetMessage( void ) const
785 {
786 return message;
787 }
788
clear()789 void Battle::Status::clear()
790 {
791 bar1.Clear();
792 bar2.Clear();
793 }
794
ArmiesOrder()795 Battle::ArmiesOrder::ArmiesOrder()
796 : orders( nullptr )
797 , army_color2( 0 )
798 {
799 const fheroes2::Size sz( ARMYORDERW, ARMYORDERW );
800
801 sf_color[0].resize( sz.width, sz.height );
802 sf_color[0].reset();
803 fheroes2::DrawBorder( sf_color[0], fheroes2::GetColorId( 0xe0, 0xe0, 0 ) );
804
805 sf_color[1].resize( sz.width, sz.height );
806 sf_color[1].reset();
807 fheroes2::DrawBorder( sf_color[1], fheroes2::GetColorId( 0xe0, 0x48, 0 ) );
808
809 sf_color[2].resize( sz.width, sz.height );
810 sf_color[2].reset();
811 fheroes2::DrawBorder( sf_color[2], fheroes2::GetColorId( 0x90, 0xc0, 0 ) );
812 }
813
Set(const fheroes2::Rect & rt,const Units * units,int color)814 void Battle::ArmiesOrder::Set( const fheroes2::Rect & rt, const Units * units, int color )
815 {
816 area = rt;
817 orders = units;
818 army_color2 = color;
819 if ( units )
820 rects.reserve( units->size() );
821 }
822
QueueEventProcessing(std::string & msg,const fheroes2::Point & offset)823 void Battle::ArmiesOrder::QueueEventProcessing( std::string & msg, const fheroes2::Point & offset )
824 {
825 LocalEvent & le = LocalEvent::Get();
826
827 for ( std::vector<UnitPos>::const_iterator it = rects.begin(); it != rects.end(); ++it )
828 if ( ( *it ).first ) {
829 const fheroes2::Rect unitRoi = ( *it ).second + offset;
830 if ( le.MouseCursor( unitRoi ) ) {
831 msg = _( "View %{monster} info" );
832 StringReplace( msg, "%{monster}", ( *it ).first->GetName() );
833 }
834
835 const Unit & unit = *( *it ).first;
836
837 if ( le.MouseClickLeft( unitRoi ) )
838 Dialog::ArmyInfo( unit, Dialog::READONLY | Dialog::BUTTONS, unit.isReflect() );
839 else if ( le.MousePressRight( unitRoi ) )
840 Dialog::ArmyInfo( unit, Dialog::READONLY, unit.isReflect() );
841 }
842 }
843
RedrawUnit(const fheroes2::Rect & pos,const Battle::Unit & unit,bool revert,bool current,fheroes2::Image & output) const844 void Battle::ArmiesOrder::RedrawUnit( const fheroes2::Rect & pos, const Battle::Unit & unit, bool revert, bool current, fheroes2::Image & output ) const
845 {
846 const fheroes2::Sprite & mons32 = fheroes2::AGG::GetICN( ICN::MONS32, unit.GetSpriteIndex() );
847
848 // background
849 fheroes2::Fill( output, pos.x, pos.y, pos.width, pos.height, fheroes2::GetColorId( 0x33, 0x33, 0x33 ) );
850 // mons32 sprite
851 fheroes2::Blit( mons32, output, pos.x + ( pos.width - mons32.width() ) / 2, pos.y + pos.height - mons32.height() - ( mons32.height() + 3 < pos.height ? 3 : 0 ),
852 revert );
853
854 // window
855 if ( current )
856 fheroes2::Blit( sf_color[0], output, pos.x + 1, pos.y + 1 );
857 else if ( unit.Modes( Battle::TR_MOVED ) )
858 fheroes2::Blit( sf_color[1], output, pos.x + 1, pos.y + 1 );
859 else
860 fheroes2::Blit( sf_color[2], output, pos.x + 1, pos.y + 1 );
861
862 // number
863 Text number( std::to_string( unit.GetCount() ), Font::SMALL );
864 number.Blit( pos.x + 2, pos.y + 2, output );
865 }
866
Redraw(const Unit * current,fheroes2::Image & output)867 void Battle::ArmiesOrder::Redraw( const Unit * current, fheroes2::Image & output )
868 {
869 if ( orders ) {
870 const int32_t ow = ARMYORDERW + 2;
871
872 const int32_t validUnitCount = static_cast<int32_t>( std::count_if( orders->begin(), orders->end(), []( const Unit * unit ) { return unit->isValid(); } ) );
873
874 int32_t ox = area.x + ( area.width - ow * validUnitCount ) / 2;
875 int32_t oy = area.y;
876
877 fheroes2::Rect::x = ox;
878 fheroes2::Rect::y = oy;
879 fheroes2::Rect::height = ow;
880
881 rects.clear();
882
883 for ( Units::const_iterator it = orders->begin(); it != orders->end(); ++it )
884 if ( *it && ( *it )->isValid() ) {
885 rects.emplace_back( *it, fheroes2::Rect( ox, oy, ow, ow ) );
886 RedrawUnit( rects.back().second, **it, ( **it ).GetColor() == army_color2, current == *it, output );
887 ox += ow;
888 fheroes2::Rect::width += ow;
889 }
890 }
891 }
892
Interface(Arena & a,s32 center)893 Battle::Interface::Interface( Arena & a, s32 center )
894 : arena( a )
895 , _surfaceInnerArea( 0, 0, fheroes2::Display::DEFAULT_WIDTH, fheroes2::Display::DEFAULT_HEIGHT )
896 , _mainSurface( fheroes2::Display::DEFAULT_WIDTH, fheroes2::Display::DEFAULT_HEIGHT )
897 , icn_cbkg( ICN::UNKNOWN )
898 , icn_frng( ICN::UNKNOWN )
899 , humanturn_spell( Spell::NONE )
900 , humanturn_exit( true )
901 , humanturn_redraw( true )
902 , animation_flags_frame( 0 )
903 , catapult_frame( 0 )
904 , _contourColor( 110 )
905 , _brightLandType( false )
906 , _currentUnit( nullptr )
907 , _movingUnit( nullptr )
908 , _flyingUnit( nullptr )
909 , b_current_sprite( nullptr )
910 , index_pos( -1 )
911 , teleport_src( -1 )
912 , listlog( nullptr )
913 , _cursorRestorer( true, Cursor::WAR_POINTER )
914 , _bridgeAnimation( { false, BridgeMovementAnimation::UP_POSITION } )
915 {
916 const Settings & conf = Settings::Get();
917
918 // border
919 const fheroes2::Display & display = fheroes2::Display::instance();
920
921 _interfacePosition = fheroes2::Rect( ( display.width() - fheroes2::Display::DEFAULT_WIDTH ) / 2, ( display.height() - fheroes2::Display::DEFAULT_HEIGHT ) / 2,
922 _surfaceInnerArea.width, _surfaceInnerArea.height );
923 border.SetPosition( _interfacePosition.x - BORDERWIDTH, _interfacePosition.y - BORDERWIDTH, fheroes2::Display::DEFAULT_WIDTH, fheroes2::Display::DEFAULT_HEIGHT );
924
925 // cover
926 const bool trees = !Maps::ScanAroundObject( center, MP2::OBJ_TREES ).empty();
927 const Maps::Tiles & tile = world.GetTiles( center );
928
929 const int groundType = tile.GetGround();
930 _brightLandType
931 = ( groundType == Maps::Ground::SNOW || groundType == Maps::Ground::DESERT || groundType == Maps::Ground::WASTELAND || groundType == Maps::Ground::BEACH );
932 if ( _brightLandType ) {
933 _contourColor = 108;
934 }
935
936 switch ( groundType ) {
937 case Maps::Ground::DESERT:
938 icn_cbkg = ICN::CBKGDSRT;
939 icn_frng = ICN::FRNG0004;
940 break;
941 case Maps::Ground::SNOW:
942 icn_cbkg = trees ? ICN::CBKGSNTR : ICN::CBKGSNMT;
943 icn_frng = trees ? ICN::FRNG0006 : ICN::FRNG0007;
944 break;
945 case Maps::Ground::SWAMP:
946 icn_cbkg = ICN::CBKGSWMP;
947 icn_frng = ICN::FRNG0008;
948 break;
949 case Maps::Ground::WASTELAND:
950 icn_cbkg = ICN::CBKGCRCK;
951 icn_frng = ICN::FRNG0003;
952 break;
953 case Maps::Ground::BEACH:
954 icn_cbkg = ICN::CBKGBEAC;
955 icn_frng = ICN::FRNG0002;
956 break;
957 case Maps::Ground::LAVA:
958 icn_cbkg = ICN::CBKGLAVA;
959 icn_frng = ICN::FRNG0005;
960 break;
961 case Maps::Ground::DIRT:
962 icn_cbkg = trees ? ICN::CBKGDITR : ICN::CBKGDIMT;
963 icn_frng = trees ? ICN::FRNG0010 : ICN::FRNG0009;
964 break;
965 case Maps::Ground::GRASS:
966 icn_cbkg = trees ? ICN::CBKGGRTR : ICN::CBKGGRMT;
967 icn_frng = trees ? ICN::FRNG0011 : ICN::FRNG0012;
968 break;
969 case Maps::Ground::WATER:
970 icn_cbkg = ICN::CBKGWATR;
971 icn_frng = ICN::FRNG0013;
972 break;
973 default:
974 break;
975 }
976
977 // hexagon
978 sf_hexagon = DrawHexagon( fheroes2::GetColorId( 0x68, 0x8C, 0x04 ) );
979 sf_cursor = DrawHexagonShadow( 2 );
980 sf_shadow = DrawHexagonShadow( 4 );
981
982 btn_auto.setICNInfo( ICN::TEXTBAR, 4, 5 );
983 btn_settings.setICNInfo( ICN::TEXTBAR, 6, 7 );
984
985 // opponents
986 opponent1 = arena.GetCommander1() ? new OpponentSprite( _surfaceInnerArea, arena.GetCommander1(), false ) : nullptr;
987 opponent2 = arena.GetCommander2() ? new OpponentSprite( _surfaceInnerArea, arena.GetCommander2(), true ) : nullptr;
988
989 if ( Arena::GetCastle() )
990 main_tower = fheroes2::Rect( 570, 145, 70, 70 );
991
992 const fheroes2::Rect & area = border.GetArea();
993
994 const fheroes2::Rect autoRect = btn_auto.area();
995 const fheroes2::Rect settingsRect = btn_settings.area();
996 btn_auto.setPosition( area.x, area.y + area.height - settingsRect.height - autoRect.height );
997 btn_settings.setPosition( area.x, area.y + area.height - settingsRect.height );
998
999 if ( conf.ExtBattleSoftWait() ) {
1000 btn_wait.setICNInfo( ICN::BATTLEWAIT, 0, 1 );
1001 btn_skip.setICNInfo( ICN::BATTLESKIP, 0, 1 );
1002
1003 const fheroes2::Rect waitRect = btn_wait.area();
1004 const fheroes2::Rect skipRect = btn_skip.area();
1005 btn_wait.setPosition( area.x + area.width - waitRect.width, area.y + area.height - skipRect.height - waitRect.height );
1006 btn_skip.setPosition( area.x + area.width - skipRect.width, area.y + area.height - skipRect.height );
1007 }
1008 else {
1009 btn_skip.setICNInfo( ICN::TEXTBAR, 0, 1 );
1010 btn_skip.setPosition( area.x + area.width - btn_skip.area().width, area.y + area.height - btn_skip.area().height );
1011 }
1012
1013 status.SetPosition( area.x + settingsRect.width, btn_auto.area().y );
1014
1015 listlog = new StatusListBox();
1016
1017 if ( listlog )
1018 listlog->SetPosition( area.x, area.y + area.height - status.height );
1019 status.SetLogs( listlog );
1020
1021 AGG::ResetMixer();
1022 AGG::PlaySound( M82::PREBATTL );
1023 }
1024
~Interface()1025 Battle::Interface::~Interface()
1026 {
1027 AGG::ResetMixer();
1028
1029 if ( listlog )
1030 delete listlog;
1031 if ( opponent1 )
1032 delete opponent1;
1033 if ( opponent2 )
1034 delete opponent2;
1035 }
1036
SetArmiesOrder(const Units * units)1037 void Battle::Interface::SetArmiesOrder( const Units * units )
1038 {
1039 armies_order.Set( GetArea(), units, arena.GetArmyColor2() );
1040 }
1041
GetArea(void) const1042 const fheroes2::Rect & Battle::Interface::GetArea( void ) const
1043 {
1044 return _surfaceInnerArea;
1045 }
1046
GetMouseCursor() const1047 fheroes2::Point Battle::Interface::GetMouseCursor() const
1048 {
1049 return LocalEvent::Get().GetMouseCursor() - _interfacePosition.getPosition();
1050 }
1051
SetStatus(const std::string & msg,bool top)1052 void Battle::Interface::SetStatus( const std::string & msg, bool top )
1053 {
1054 if ( top ) {
1055 status.SetMessage( msg, true );
1056 status.SetMessage( "", false );
1057 }
1058 else {
1059 status.SetMessage( msg );
1060 }
1061 humanturn_redraw = true;
1062 }
1063
UpdateContourColor()1064 void Battle::Interface::UpdateContourColor()
1065 {
1066 ++_contourCycle;
1067
1068 if ( _brightLandType ) {
1069 static const uint8_t contourColorTable[] = {108, 115, 122, 129, 122, 115};
1070 _contourColor = contourColorTable[_contourCycle % sizeof( contourColorTable )];
1071 }
1072 else {
1073 static const uint8_t contourColorTable[] = {110, 114, 118, 122, 126, 122, 118, 114};
1074 _contourColor = contourColorTable[_contourCycle % sizeof( contourColorTable )];
1075 }
1076 }
1077
fullRedraw()1078 void Battle::Interface::fullRedraw()
1079 {
1080 if ( !_background ) {
1081 _background.reset( new fheroes2::StandardWindow( fheroes2::Display::DEFAULT_WIDTH, fheroes2::Display::DEFAULT_HEIGHT ) );
1082 }
1083
1084 Redraw();
1085 }
1086
Redraw(void)1087 void Battle::Interface::Redraw( void )
1088 {
1089 RedrawPartialStart();
1090 RedrawPartialFinish();
1091 }
1092
RedrawPartialStart()1093 void Battle::Interface::RedrawPartialStart()
1094 {
1095 RedrawCover();
1096 RedrawArmies();
1097 }
1098
RedrawPartialFinish()1099 void Battle::Interface::RedrawPartialFinish()
1100 {
1101 fheroes2::Display & display = fheroes2::Display::instance();
1102
1103 if ( Settings::Get().ExtBattleShowBattleOrder() )
1104 armies_order.Redraw( _currentUnit, _mainSurface );
1105
1106 #ifdef WITH_DEBUG
1107 if ( IS_DEVEL() ) {
1108 const Board & board = *Arena::GetBoard();
1109 for ( Board::const_iterator it = board.begin(); it != board.end(); ++it ) {
1110 uint32_t distance = arena.CalculateMoveDistance( it->GetIndex() );
1111 if ( distance != MAX_MOVE_COST ) {
1112 Text text( std::to_string( distance ), Font::SMALL );
1113 text.Blit( ( *it ).GetPos().x + 20, ( *it ).GetPos().y + 22, _mainSurface );
1114 }
1115 }
1116 }
1117 #endif
1118
1119 fheroes2::Blit( _mainSurface, display, _interfacePosition.x, _interfacePosition.y );
1120 RedrawInterface();
1121
1122 display.render();
1123 }
1124
RedrawInterface(void)1125 void Battle::Interface::RedrawInterface( void )
1126 {
1127 const Settings & conf = Settings::Get();
1128
1129 status.Redraw();
1130
1131 btn_auto.draw();
1132 btn_settings.draw();
1133
1134 if ( conf.ExtBattleSoftWait() )
1135 btn_wait.draw();
1136 btn_skip.draw();
1137
1138 popup.Redraw( _interfacePosition.x + _interfacePosition.width + 60, _interfacePosition.y + _interfacePosition.height );
1139
1140 if ( listlog && listlog->isOpenLog() ) {
1141 listlog->Redraw();
1142 }
1143 }
1144
RedrawArmies()1145 void Battle::Interface::RedrawArmies()
1146 {
1147 const Castle * castle = Arena::GetCastle();
1148
1149 const int32_t wallCellIds[ARENAH]
1150 = { Arena::CASTLE_FIRST_TOP_WALL_POS, Arena::CASTLE_TOP_ARCHER_TOWER_POS, Arena::CASTLE_SECOND_TOP_WALL_POS, Arena::CASTLE_TOP_GATE_TOWER_POS,
1151 Arena::CASTLE_GATE_POS, Arena::CASTLE_BOTTOM_GATE_TOWER_POS, Arena::CASTLE_THIRD_TOP_WALL_POS, Arena::CASTLE_BOTTOM_ARCHER_TOWER_POS,
1152 Arena::CASTLE_FOURTH_TOP_WALL_POS };
1153
1154 if ( castle == nullptr ) {
1155 RedrawKilled();
1156 }
1157
1158 for ( int32_t cellRowId = 0; cellRowId < ARENAH; ++cellRowId ) {
1159 // Redraw objects.
1160 for ( int32_t cellColumnId = 0; cellColumnId < ARENAW; ++cellColumnId ) {
1161 const int32_t cellId = cellRowId * ARENAW + cellColumnId;
1162 RedrawHighObjects( cellId );
1163 }
1164
1165 if ( castle != nullptr ) {
1166 // Redraw main tower.
1167 if ( cellRowId == 5 ) {
1168 RedrawCastleMainTower( *castle );
1169 }
1170 else if ( cellRowId == 7 ) { // Redraw catapult.
1171 RedrawCastle2( *castle, Arena::CATAPULT_POS );
1172 }
1173
1174 std::vector<const Unit *> deadTroopBeforeWall;
1175 std::vector<const Unit *> deadTroopAfterWall;
1176
1177 std::vector<const Unit *> troopCounterBeforeWall;
1178 std::vector<const Unit *> troopCounterAfterWall;
1179
1180 std::vector<const Unit *> troopBeforeWall;
1181 std::vector<const Unit *> troopAfterWall;
1182
1183 std::vector<const Unit *> movingTroopBeforeWall;
1184 std::vector<const Unit *> movingTroopAfterWall;
1185
1186 const int32_t wallCellId = wallCellIds[cellRowId];
1187
1188 for ( int32_t cellColumnId = 0; cellColumnId < ARENAW; ++cellColumnId ) {
1189 const int32_t cellId = cellRowId * ARENAW + cellColumnId;
1190
1191 bool isCellBefore = true;
1192 if ( cellRowId < 5 ) {
1193 isCellBefore = cellId < wallCellId;
1194 }
1195 else {
1196 isCellBefore = cellId > wallCellId;
1197 if ( ( wallCellId == Arena::CASTLE_THIRD_TOP_WALL_POS || wallCellId == Arena::CASTLE_FOURTH_TOP_WALL_POS )
1198 && Board::GetCell( wallCellId )->GetObject() == 0 ) {
1199 isCellBefore = false;
1200 }
1201 }
1202
1203 const std::vector<const Unit *> & deadUnits = arena.GetGraveyardTroops( cellId );
1204 for ( size_t i = 0; i < deadUnits.size(); ++i ) {
1205 if ( deadUnits[i] && cellId != deadUnits[i]->GetTailIndex() ) {
1206 if ( isCellBefore ) {
1207 deadTroopBeforeWall.emplace_back( deadUnits[i] );
1208 }
1209 else {
1210 deadTroopAfterWall.emplace_back( deadUnits[i] );
1211 }
1212 }
1213 }
1214
1215 const Unit * unitOnCell = Board::GetCell( cellId )->GetUnit();
1216 if ( unitOnCell == nullptr || _flyingUnit == unitOnCell || cellId == unitOnCell->GetTailIndex() ) {
1217 continue;
1218 }
1219
1220 if ( _movingUnit != unitOnCell && unitOnCell->isValid() ) {
1221 const int unitAnimState = unitOnCell->GetAnimationState();
1222 const bool isStaticUnit = unitAnimState == Monster_Info::STATIC || unitAnimState == Monster_Info::IDLE;
1223
1224 if ( isCellBefore ) {
1225 if ( isStaticUnit ) {
1226 troopCounterBeforeWall.emplace_back( unitOnCell );
1227 }
1228 troopBeforeWall.emplace_back( unitOnCell );
1229 }
1230 else {
1231 if ( isStaticUnit ) {
1232 troopCounterAfterWall.emplace_back( unitOnCell );
1233 }
1234 troopAfterWall.emplace_back( unitOnCell );
1235 }
1236 }
1237 else {
1238 if ( isCellBefore ) {
1239 movingTroopBeforeWall.emplace_back( unitOnCell );
1240 }
1241 else {
1242 movingTroopAfterWall.emplace_back( unitOnCell );
1243 }
1244 }
1245 }
1246
1247 for ( size_t i = 0; i < deadTroopBeforeWall.size(); ++i ) {
1248 RedrawTroopSprite( *deadTroopBeforeWall[i] );
1249 }
1250
1251 for ( size_t i = 0; i < troopBeforeWall.size(); ++i ) {
1252 RedrawTroopSprite( *troopBeforeWall[i] );
1253 }
1254
1255 for ( size_t i = 0; i < troopCounterBeforeWall.size(); ++i ) {
1256 RedrawTroopCount( *troopCounterBeforeWall[i] );
1257 }
1258
1259 for ( size_t i = 0; i < movingTroopBeforeWall.size(); ++i ) {
1260 RedrawTroopSprite( *movingTroopBeforeWall[i] );
1261 }
1262
1263 RedrawCastle2( *castle, wallCellId );
1264
1265 for ( size_t i = 0; i < deadTroopAfterWall.size(); ++i ) {
1266 RedrawTroopSprite( *deadTroopAfterWall[i] );
1267 }
1268
1269 for ( size_t i = 0; i < troopAfterWall.size(); ++i ) {
1270 RedrawTroopSprite( *troopAfterWall[i] );
1271 }
1272
1273 for ( size_t i = 0; i < troopCounterAfterWall.size(); ++i ) {
1274 RedrawTroopCount( *troopCounterAfterWall[i] );
1275 }
1276
1277 for ( size_t i = 0; i < movingTroopAfterWall.size(); ++i ) {
1278 RedrawTroopSprite( *movingTroopAfterWall[i] );
1279 }
1280 }
1281 else {
1282 std::vector<const Unit *> troopCounter;
1283 std::vector<const Unit *> troop;
1284 std::vector<const Unit *> movingTroop;
1285
1286 // Redraw monsters.
1287 for ( int32_t cellColumnId = 0; cellColumnId < ARENAW; ++cellColumnId ) {
1288 const int32_t cellId = cellRowId * ARENAW + cellColumnId;
1289
1290 const Unit * unitOnCell = Board::GetCell( cellId )->GetUnit();
1291 if ( unitOnCell == nullptr || _flyingUnit == unitOnCell || cellId == unitOnCell->GetTailIndex() ) {
1292 continue;
1293 }
1294
1295 if ( _movingUnit != unitOnCell && unitOnCell->isValid() ) {
1296 const int unitAnimState = unitOnCell->GetAnimationState();
1297 const bool isStaticUnit = unitAnimState == Monster_Info::STATIC || unitAnimState == Monster_Info::IDLE;
1298 if ( isStaticUnit ) {
1299 troopCounter.emplace_back( unitOnCell );
1300 }
1301
1302 troop.emplace_back( unitOnCell );
1303 }
1304 else {
1305 movingTroop.emplace_back( unitOnCell );
1306 }
1307 }
1308
1309 // Redraw monster counters.
1310 for ( size_t i = 0; i < troop.size(); ++i ) {
1311 RedrawTroopSprite( *troop[i] );
1312 }
1313
1314 for ( size_t i = 0; i < troopCounter.size(); ++i ) {
1315 RedrawTroopCount( *troopCounter[i] );
1316 }
1317
1318 for ( size_t i = 0; i < movingTroop.size(); ++i ) {
1319 RedrawTroopSprite( *movingTroop[i] );
1320 }
1321 }
1322
1323 // Redraw heroes.
1324 if ( cellRowId == 2 ) {
1325 RedrawOpponents();
1326 }
1327 }
1328
1329 if ( _flyingUnit ) {
1330 RedrawTroopSprite( *_flyingUnit );
1331 }
1332 }
1333
RedrawOpponents(void)1334 void Battle::Interface::RedrawOpponents( void )
1335 {
1336 if ( opponent1 )
1337 opponent1->Redraw( _mainSurface );
1338 if ( opponent2 )
1339 opponent2->Redraw( _mainSurface );
1340
1341 RedrawOpponentsFlags();
1342 }
1343
RedrawOpponentsFlags(void)1344 void Battle::Interface::RedrawOpponentsFlags( void )
1345 {
1346 if ( opponent1 ) {
1347 int icn = ICN::UNKNOWN;
1348
1349 switch ( arena.GetArmyColor1() ) {
1350 case Color::BLUE:
1351 icn = ICN::HEROFL00;
1352 break;
1353 case Color::GREEN:
1354 icn = ICN::HEROFL01;
1355 break;
1356 case Color::RED:
1357 icn = ICN::HEROFL02;
1358 break;
1359 case Color::YELLOW:
1360 icn = ICN::HEROFL03;
1361 break;
1362 case Color::ORANGE:
1363 icn = ICN::HEROFL04;
1364 break;
1365 case Color::PURPLE:
1366 icn = ICN::HEROFL05;
1367 break;
1368 default:
1369 icn = ICN::HEROFL06;
1370 break;
1371 }
1372
1373 const fheroes2::Sprite & flag = fheroes2::AGG::GetICN( icn, ICN::AnimationFrame( icn, 0, animation_flags_frame ) );
1374 fheroes2::Blit( flag, _mainSurface, opponent1->Offset().x + OpponentSprite::HERO_X_OFFSET + flag.x(),
1375 opponent1->Offset().y + OpponentSprite::LEFT_HERO_Y_OFFSET + flag.y() );
1376 }
1377
1378 if ( opponent2 ) {
1379 int icn = ICN::UNKNOWN;
1380
1381 switch ( arena.GetForce2().GetColor() ) {
1382 case Color::BLUE:
1383 icn = ICN::HEROFL00;
1384 break;
1385 case Color::GREEN:
1386 icn = ICN::HEROFL01;
1387 break;
1388 case Color::RED:
1389 icn = ICN::HEROFL02;
1390 break;
1391 case Color::YELLOW:
1392 icn = ICN::HEROFL03;
1393 break;
1394 case Color::ORANGE:
1395 icn = ICN::HEROFL04;
1396 break;
1397 case Color::PURPLE:
1398 icn = ICN::HEROFL05;
1399 break;
1400 default:
1401 icn = ICN::HEROFL06;
1402 break;
1403 }
1404
1405 const fheroes2::Sprite & flag = fheroes2::AGG::GetICN( icn, ICN::AnimationFrame( icn, 0, animation_flags_frame ) );
1406 const fheroes2::Point offset = opponent2->Offset();
1407 fheroes2::Blit( flag, _mainSurface, offset.x + fheroes2::Display::DEFAULT_WIDTH - OpponentSprite::HERO_X_OFFSET - ( flag.x() + flag.width() ),
1408 offset.y + OpponentSprite::RIGHT_HERO_Y_OFFSET + flag.y(), true );
1409 }
1410 }
1411
GetTroopPosition(const Battle::Unit & unit,const fheroes2::Sprite & sprite)1412 fheroes2::Point GetTroopPosition( const Battle::Unit & unit, const fheroes2::Sprite & sprite )
1413 {
1414 const fheroes2::Rect & rt = unit.GetRectPosition();
1415
1416 int32_t offsetX = 0;
1417 if ( unit.isReflect() ) {
1418 if ( unit.isWide() ) {
1419 offsetX = rt.x + ( rt.width / 2 + rt.width / 4 ) - sprite.width() - sprite.x() + 1;
1420 }
1421 else {
1422 offsetX = rt.x + ( rt.width / 2 ) - sprite.width() - sprite.x() + 1;
1423 }
1424 }
1425 else {
1426 if ( unit.isWide() ) {
1427 offsetX = rt.x + ( rt.width / 4 ) + sprite.x();
1428 }
1429 else {
1430 offsetX = rt.x + ( rt.width / 2 ) + sprite.x();
1431 }
1432 }
1433
1434 const int32_t offsetY = rt.y + rt.height + sprite.y() + cellYOffset;
1435
1436 return fheroes2::Point( offsetX, offsetY );
1437 }
1438
RedrawTroopSprite(const Unit & unit)1439 void Battle::Interface::RedrawTroopSprite( const Unit & unit )
1440 {
1441 if ( b_current_sprite && _currentUnit == &unit ) {
1442 drawTroopSprite( unit, *b_current_sprite );
1443 }
1444 else if ( unit.Modes( SP_STONE ) ) {
1445 // Current monster can't be active if it's under Stunning effect.
1446 const int monsterIcnId = unit.GetMonsterSprite();
1447 fheroes2::Sprite monsterSprite = fheroes2::AGG::GetICN( monsterIcnId, unit.GetFrame() );
1448 fheroes2::ApplyPalette( monsterSprite, PAL::GetPalette( PAL::PaletteType::GRAY ) );
1449 drawTroopSprite( unit, monsterSprite );
1450 }
1451 else if ( unit.Modes( CAP_MIRRORIMAGE ) ) {
1452 fheroes2::Sprite monsterSprite;
1453
1454 if ( _currentUnit == &unit && b_current_sprite != nullptr ) {
1455 monsterSprite = *b_current_sprite;
1456 }
1457 else {
1458 const int monsterIcnId = unit.GetMonsterSprite();
1459 monsterSprite = fheroes2::AGG::GetICN( monsterIcnId, unit.GetFrame() );
1460 }
1461
1462 fheroes2::ApplyPalette( monsterSprite, PAL::GetPalette( PAL::PaletteType::MIRROR_IMAGE ) );
1463
1464 const fheroes2::Point drawnPosition = drawTroopSprite( unit, monsterSprite );
1465
1466 if ( _currentUnit == &unit && b_current_sprite == nullptr ) {
1467 // Current unit's turn which is idling.
1468 const fheroes2::Sprite & monsterContour = fheroes2::CreateContour( monsterSprite, _contourColor );
1469 fheroes2::Blit( monsterContour, _mainSurface, drawnPosition.x, drawnPosition.y, unit.isReflect() );
1470 }
1471 }
1472 else {
1473 const int monsterIcnId = unit.GetMonsterSprite();
1474 const bool isCurrentMonsterAction = ( _currentUnit == &unit && b_current_sprite != nullptr );
1475
1476 const fheroes2::Sprite & monsterSprite = isCurrentMonsterAction ? *b_current_sprite : fheroes2::AGG::GetICN( monsterIcnId, unit.GetFrame() );
1477
1478 const fheroes2::Point drawnPosition = drawTroopSprite( unit, monsterSprite );
1479
1480 if ( _currentUnit == &unit && b_current_sprite == nullptr ) {
1481 // Current unit's turn which is idling.
1482 const fheroes2::Sprite & monsterContour = fheroes2::CreateContour( monsterSprite, _contourColor );
1483 fheroes2::Blit( monsterContour, _mainSurface, drawnPosition.x, drawnPosition.y, unit.isReflect() );
1484 }
1485 }
1486 }
1487
drawTroopSprite(const Unit & unit,const fheroes2::Sprite & troopSprite)1488 fheroes2::Point Battle::Interface::drawTroopSprite( const Unit & unit, const fheroes2::Sprite & troopSprite )
1489 {
1490 const fheroes2::Rect & rt = unit.GetRectPosition();
1491 fheroes2::Point sp = GetTroopPosition( unit, troopSprite );
1492
1493 if ( _movingUnit == &unit ) {
1494 // Monster is moving.
1495 // Here we're getting the first frame and then based on the offset from the first frame we calculate the position of the current frame.
1496 // TODO: verify if it's the correct way as we have issues for monster movement animation.
1497 const int monsterIcnId = unit.GetMonsterSprite();
1498 const fheroes2::Sprite & firstMonsterFrame = fheroes2::AGG::GetICN( monsterIcnId, _movingUnit->animation.firstFrame() );
1499 const s32 ox = troopSprite.x() - firstMonsterFrame.x();
1500
1501 if ( _movingUnit->animation.animationLength() ) {
1502 const int32_t cx = _movingPos.x - rt.x;
1503 const int32_t cy = _movingPos.y - rt.y;
1504
1505 // TODO: use offset X from bin file for ground movement
1506 // cx/cy is sprite size
1507 // Frame count: one tile of movement goes through all stages of animation
1508 // sp is sprite drawing offset
1509 sp.y += static_cast<int32_t>( _movingUnit->animation.movementProgress() * cy );
1510 if ( 0 != Sign( cy ) )
1511 sp.x -= Sign( cx ) * ox / 2;
1512 }
1513 }
1514 else if ( _flyingUnit == &unit ) {
1515 // Monster is flying.
1516 const int32_t cx = _flyingPos.x - rt.x;
1517 const int32_t cy = _flyingPos.y - rt.y;
1518
1519 const double movementProgress = _flyingUnit->animation.movementProgress();
1520
1521 sp.x += cx + static_cast<int32_t>( ( _movingPos.x - _flyingPos.x ) * movementProgress );
1522 sp.y += cy + static_cast<int32_t>( ( _movingPos.y - _flyingPos.y ) * movementProgress );
1523 }
1524
1525 fheroes2::AlphaBlit( troopSprite, _mainSurface, sp.x, sp.y, unit.GetCustomAlpha(), unit.isReflect() );
1526
1527 return sp;
1528 }
1529
RedrawTroopCount(const Unit & unit)1530 void Battle::Interface::RedrawTroopCount( const Unit & unit )
1531 {
1532 const fheroes2::Rect & rt = unit.GetRectPosition();
1533 const fheroes2::Sprite & bar = fheroes2::AGG::GetICN( ICN::TEXTBAR, GetIndexIndicator( unit ) );
1534 const bool isReflected = unit.isReflect();
1535
1536 const int32_t monsterIndex = unit.GetHeadIndex();
1537 const int tileInFront = Board::GetIndexDirection( monsterIndex, isReflected ? Battle::LEFT : Battle::RIGHT );
1538 const bool isValidFrontMonster = ( monsterIndex / ARENAW ) == ( tileInFront == ARENAW );
1539
1540 s32 sx = rt.x + ( isReflected ? -7 : rt.width - 13 );
1541 const s32 sy = rt.y + rt.height - bar.height() - ( isReflected ? 21 : 9 );
1542
1543 int xOffset = unit.animation.getTroopCountOffset( isReflected );
1544 // check if has unit standing in front
1545 if ( xOffset > 0 && isValidFrontMonster && Board::isValidIndex( tileInFront ) && Board::GetCell( tileInFront )->GetUnit() != nullptr )
1546 xOffset = 0;
1547
1548 sx += isReflected ? -xOffset : xOffset;
1549
1550 fheroes2::Blit( bar, _mainSurface, sx, sy );
1551
1552 Text text( GetStringShort( unit.GetCount() ), Font::SMALL );
1553 text.Blit( sx + ( bar.width() - text.w() ) / 2, sy, _mainSurface );
1554 }
1555
RedrawCover()1556 void Battle::Interface::RedrawCover()
1557 {
1558 const Settings & conf = Settings::Get();
1559 const Board & board = *Arena::GetBoard();
1560
1561 RedrawCoverStatic( conf, board );
1562
1563 const Bridge * bridge = Arena::GetBridge();
1564 if ( bridge && ( bridge->isDown() || _bridgeAnimation.animationIsRequired ) ) {
1565 uint32_t spriteIndex = bridge->isDestroy() ? BridgeMovementAnimation::DESTROYED : BridgeMovementAnimation::DOWN_POSITION;
1566
1567 if ( _bridgeAnimation.animationIsRequired ) {
1568 spriteIndex = _bridgeAnimation.currentFrameId;
1569 }
1570
1571 const fheroes2::Sprite & bridgeImage = fheroes2::AGG::GetICN( ICN::Get4Castle( Arena::GetCastle()->GetRace() ), spriteIndex );
1572 fheroes2::Blit( bridgeImage, _mainSurface, bridgeImage.x(), bridgeImage.y() );
1573 }
1574
1575 // cursor
1576 const Cell * cell = Board::GetCell( index_pos );
1577 const int cursorType = Cursor::Get().Themes();
1578
1579 if ( cell && _currentUnit && conf.BattleShowMouseShadow() ) {
1580 std::set<const Cell *> highlightCells;
1581
1582 if ( humanturn_spell.isValid() ) {
1583 switch ( humanturn_spell.GetID() ) {
1584 case Spell::COLDRING: {
1585 const Indexes around = Board::GetAroundIndexes( index_pos );
1586 for ( size_t i = 0; i < around.size(); ++i ) {
1587 const Cell * aroundCell = Board::GetCell( around[i] );
1588 if ( aroundCell != nullptr ) {
1589 highlightCells.emplace( aroundCell );
1590 }
1591 }
1592 break;
1593 }
1594 case Spell::FIREBALL:
1595 case Spell::METEORSHOWER: {
1596 highlightCells.emplace( cell );
1597 const Indexes around = Board::GetAroundIndexes( index_pos );
1598 for ( size_t i = 0; i < around.size(); ++i ) {
1599 const Cell * aroundCell = Board::GetCell( around[i] );
1600 if ( aroundCell != nullptr ) {
1601 highlightCells.emplace( aroundCell );
1602 }
1603 }
1604 break;
1605 }
1606 case Spell::FIREBLAST: {
1607 highlightCells.emplace( cell );
1608 const Indexes around = Board::GetAroundIndexes( index_pos );
1609 for ( size_t i = 0; i < around.size(); ++i ) {
1610 const Cell * aroundCell = Board::GetCell( around[i] );
1611 if ( aroundCell != nullptr ) {
1612 highlightCells.emplace( aroundCell );
1613 }
1614
1615 const Indexes aroundTwice = Board::GetAroundIndexes( around[i] );
1616 for ( size_t j = 0; j < aroundTwice.size(); ++j ) {
1617 const Cell * aroundCellTwice = Board::GetCell( aroundTwice[j] );
1618 if ( aroundCellTwice != nullptr ) {
1619 highlightCells.emplace( aroundCellTwice );
1620 }
1621 }
1622 }
1623 break;
1624 }
1625 default:
1626 highlightCells.emplace( cell );
1627 }
1628 }
1629 else if ( _currentUnit->isAbilityPresent( fheroes2::MonsterAbilityType::AREA_SHOT )
1630 && ( cursorType == Cursor::WAR_ARROW || cursorType == Cursor::WAR_BROKENARROW ) ) {
1631 highlightCells.emplace( cell );
1632 const Indexes around = Board::GetAroundIndexes( index_pos );
1633 for ( size_t i = 0; i < around.size(); ++i ) {
1634 const Cell * aroundCell = Board::GetCell( around[i] );
1635 if ( aroundCell != nullptr ) {
1636 highlightCells.emplace( aroundCell );
1637 }
1638 }
1639 }
1640 else if ( _currentUnit->isWide() && ( cursorType == Cursor::WAR_MOVE || cursorType == Cursor::WAR_FLY ) ) {
1641 const Position pos = Position::GetReachable( *_currentUnit, index_pos );
1642
1643 assert( pos.GetHead() != nullptr );
1644 assert( pos.GetTail() != nullptr );
1645
1646 highlightCells.emplace( pos.GetHead() );
1647 highlightCells.emplace( pos.GetTail() );
1648 }
1649 else if ( cursorType == Cursor::SWORD_TOPLEFT || cursorType == Cursor::SWORD_TOPRIGHT || cursorType == Cursor::SWORD_BOTTOMLEFT
1650 || cursorType == Cursor::SWORD_BOTTOMRIGHT || cursorType == Cursor::SWORD_LEFT || cursorType == Cursor::SWORD_RIGHT ) {
1651 highlightCells.emplace( cell );
1652
1653 int direction = 0;
1654 if ( cursorType == Cursor::SWORD_TOPLEFT ) {
1655 direction = BOTTOM_RIGHT;
1656 }
1657 else if ( cursorType == Cursor::SWORD_TOPRIGHT ) {
1658 direction = BOTTOM_LEFT;
1659 }
1660 else if ( cursorType == Cursor::SWORD_BOTTOMLEFT ) {
1661 direction = TOP_RIGHT;
1662 }
1663 else if ( cursorType == Cursor::SWORD_BOTTOMRIGHT ) {
1664 direction = TOP_LEFT;
1665 }
1666 else if ( cursorType == Cursor::SWORD_LEFT ) {
1667 direction = RIGHT;
1668 }
1669 else if ( cursorType == Cursor::SWORD_RIGHT ) {
1670 direction = LEFT;
1671 }
1672 else {
1673 assert( 0 );
1674 }
1675
1676 const Position pos = Position::GetReachable( *_currentUnit, Board::GetIndexDirection( index_pos, direction ) );
1677
1678 assert( pos.GetHead() != nullptr );
1679
1680 highlightCells.emplace( pos.GetHead() );
1681
1682 if ( _currentUnit->isWide() ) {
1683 assert( pos.GetTail() != nullptr );
1684
1685 highlightCells.emplace( pos.GetTail() );
1686 }
1687
1688 if ( _currentUnit->isDoubleCellAttack() ) {
1689 // We have to invert the direction.
1690 int attackDirection = Board::GetDirection( pos.GetHead()->GetIndex(), index_pos );
1691 if ( attackDirection == 0 ) {
1692 // It happens when a creature needs to swap tail and head for an attack move.
1693 attackDirection = Board::GetDirection( pos.GetTail()->GetIndex(), index_pos );
1694 }
1695
1696 assert( attackDirection != 0 );
1697
1698 const Cell * secondAttackedCell = Board::GetCell( index_pos, attackDirection );
1699 if ( secondAttackedCell != nullptr ) {
1700 highlightCells.emplace( secondAttackedCell );
1701 }
1702 }
1703 }
1704 else {
1705 highlightCells.emplace( cell );
1706 }
1707
1708 assert( !highlightCells.empty() );
1709
1710 const HeroBase * currentCommander = arena.GetCurrentCommander();
1711 const int spellPower = ( currentCommander == nullptr ) ? 0 : currentCommander->GetPower();
1712
1713 for ( const Cell * highlightCell : highlightCells ) {
1714 bool isApplicable = highlightCell->isPassable1( false );
1715
1716 if ( isApplicable ) {
1717 const Unit * highlightedUnit = highlightCell->GetUnit();
1718
1719 isApplicable = highlightedUnit == nullptr || !humanturn_spell.isValid() || !highlightedUnit->isMagicResist( humanturn_spell, spellPower );
1720 }
1721
1722 if ( isApplicable ) {
1723 fheroes2::Blit( sf_cursor, _mainSurface, highlightCell->GetPos().x, highlightCell->GetPos().y );
1724 }
1725 }
1726 }
1727 }
1728
RedrawCoverStatic(const Settings & conf,const Board & board)1729 void Battle::Interface::RedrawCoverStatic( const Settings & conf, const Board & board )
1730 {
1731 if ( icn_cbkg != ICN::UNKNOWN ) {
1732 const fheroes2::Sprite & cbkg = fheroes2::AGG::GetICN( icn_cbkg, 0 );
1733 fheroes2::Copy( cbkg, _mainSurface );
1734 }
1735
1736 if ( icn_frng != ICN::UNKNOWN ) {
1737 const fheroes2::Sprite & frng = fheroes2::AGG::GetICN( icn_frng, 0 );
1738 fheroes2::Blit( frng, _mainSurface, frng.x(), frng.y() );
1739 }
1740
1741 if ( arena.GetICNCovr() != ICN::UNKNOWN ) {
1742 const fheroes2::Sprite & cover = fheroes2::AGG::GetICN( arena.GetICNCovr(), 0 );
1743 fheroes2::Blit( cover, _mainSurface, cover.x(), cover.y() );
1744 }
1745
1746 if ( conf.BattleShowGrid() ) { // grid
1747 for ( const Cell & cell : board ) {
1748 fheroes2::Blit( sf_hexagon, _mainSurface, cell.GetPos().x, cell.GetPos().y );
1749 }
1750 }
1751
1752 // ground obstacles
1753 for ( int32_t cellId = 0; cellId < ARENASIZE; ++cellId ) {
1754 RedrawLowObjects( cellId );
1755 }
1756
1757 const Castle * castle = Arena::GetCastle();
1758 if ( castle )
1759 RedrawCastle1( *castle );
1760
1761 if ( !_movingUnit && conf.BattleShowMoveShadow() && _currentUnit && !( _currentUnit->GetCurrentControl() & CONTROL_AI ) ) { // shadow
1762 for ( const Cell & cell : board ) {
1763 if ( cell.isReachableForHead() || cell.isReachableForTail() ) {
1764 fheroes2::Blit( sf_shadow, _mainSurface, cell.GetPos().x, cell.GetPos().y );
1765 }
1766 }
1767 }
1768 }
1769
RedrawCastle1(const Castle & castle)1770 void Battle::Interface::RedrawCastle1( const Castle & castle )
1771 {
1772 int icn_castbkg = ICN::UNKNOWN;
1773
1774 switch ( castle.GetRace() ) {
1775 default:
1776 case Race::BARB:
1777 icn_castbkg = ICN::CASTBKGB;
1778 break;
1779 case Race::KNGT:
1780 icn_castbkg = ICN::CASTBKGK;
1781 break;
1782 case Race::NECR:
1783 icn_castbkg = ICN::CASTBKGN;
1784 break;
1785 case Race::SORC:
1786 icn_castbkg = ICN::CASTBKGS;
1787 break;
1788 case Race::WRLK:
1789 icn_castbkg = ICN::CASTBKGW;
1790 break;
1791 case Race::WZRD:
1792 icn_castbkg = ICN::CASTBKGZ;
1793 break;
1794 }
1795
1796 // castle cover
1797 const fheroes2::Sprite & sprite1 = fheroes2::AGG::GetICN( icn_castbkg, 1 );
1798 fheroes2::Blit( sprite1, _mainSurface, sprite1.x(), sprite1.y() );
1799
1800 // moat
1801 if ( castle.isBuild( BUILD_MOAT ) ) {
1802 const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::MOATWHOL, 0 );
1803 fheroes2::Blit( sprite, _mainSurface, sprite.x(), sprite.y() );
1804 }
1805
1806 // top wall
1807 const fheroes2::Sprite & sprite2 = fheroes2::AGG::GetICN( icn_castbkg, castle.isFortificationBuild() ? 4 : 3 );
1808 fheroes2::Blit( sprite2, _mainSurface, sprite2.x(), sprite2.y() );
1809 }
1810
RedrawCastle2(const Castle & castle,int32_t cellId)1811 void Battle::Interface::RedrawCastle2( const Castle & castle, int32_t cellId )
1812 {
1813 const int castleIcnId = ICN::Get4Castle( castle.GetRace() );
1814
1815 if ( Arena::CATAPULT_POS == cellId ) {
1816 const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::CATAPULT, catapult_frame );
1817 fheroes2::Blit( sprite, _mainSurface, 22 + sprite.x(), 390 + sprite.y() );
1818 }
1819 else if ( Arena::CASTLE_GATE_POS == cellId ) {
1820 const Bridge * bridge = Arena::GetBridge();
1821 assert( bridge != nullptr );
1822 if ( bridge != nullptr && !bridge->isDestroy() ) {
1823 const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( castleIcnId, 4 );
1824 fheroes2::Blit( sprite, _mainSurface, sprite.x(), sprite.y() );
1825 }
1826 }
1827 else if ( Arena::CASTLE_FIRST_TOP_WALL_POS == cellId || Arena::CASTLE_SECOND_TOP_WALL_POS == cellId || Arena::CASTLE_THIRD_TOP_WALL_POS == cellId
1828 || Arena::CASTLE_FOURTH_TOP_WALL_POS == cellId ) {
1829 uint32_t index = 0;
1830
1831 switch ( cellId ) {
1832 case Arena::CASTLE_FIRST_TOP_WALL_POS:
1833 index = 5;
1834 break;
1835 case Arena::CASTLE_SECOND_TOP_WALL_POS:
1836 index = 6;
1837 break;
1838 case Arena::CASTLE_THIRD_TOP_WALL_POS:
1839 index = 7;
1840 break;
1841 case Arena::CASTLE_FOURTH_TOP_WALL_POS:
1842 index = 8;
1843 break;
1844 default:
1845 break;
1846 }
1847
1848 if ( castle.isFortificationBuild() ) {
1849 switch ( Board::GetCell( cellId )->GetObject() ) {
1850 case 0:
1851 index += 31;
1852 break;
1853 case 1:
1854 index += 35;
1855 break;
1856 case 2:
1857 index += 27;
1858 break;
1859 case 3:
1860 index += 23;
1861 break;
1862 default:
1863 break;
1864 }
1865 }
1866 else {
1867 switch ( Board::GetCell( cellId )->GetObject() ) {
1868 case 0:
1869 index += 8;
1870 break;
1871 case 1:
1872 index += 4;
1873 break;
1874 case 2:
1875 index += 0;
1876 break;
1877 default:
1878 break;
1879 }
1880 }
1881
1882 const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( castleIcnId, index );
1883 fheroes2::Blit( sprite, _mainSurface, sprite.x(), sprite.y() );
1884 }
1885 else if ( Arena::CASTLE_TOP_ARCHER_TOWER_POS == cellId ) {
1886 const Tower * ltower = Arena::GetTower( TWR_LEFT );
1887 uint32_t index = 17;
1888
1889 if ( castle.isBuild( BUILD_LEFTTURRET ) && ltower )
1890 index = ltower->isValid() ? 18 : 19;
1891
1892 const fheroes2::Sprite & towerSprite = fheroes2::AGG::GetICN( castleIcnId, index );
1893 fheroes2::Blit( towerSprite, _mainSurface, 443 + towerSprite.x(), 153 + towerSprite.y() );
1894 }
1895 else if ( Arena::CASTLE_BOTTOM_ARCHER_TOWER_POS == cellId ) {
1896 const Tower * rtower = Arena::GetTower( TWR_RIGHT );
1897 uint32_t index = 17;
1898
1899 if ( castle.isBuild( BUILD_RIGHTTURRET ) && rtower )
1900 index = rtower->isValid() ? 18 : 19;
1901
1902 const fheroes2::Sprite & towerSprite = fheroes2::AGG::GetICN( castleIcnId, index );
1903 fheroes2::Blit( towerSprite, _mainSurface, 443 + towerSprite.x(), 405 + towerSprite.y() );
1904 }
1905 else if ( Arena::CASTLE_TOP_GATE_TOWER_POS == cellId ) {
1906 const fheroes2::Sprite & towerSprite = fheroes2::AGG::GetICN( castleIcnId, 17 );
1907 fheroes2::Blit( towerSprite, _mainSurface, 399 + towerSprite.x(), 237 + towerSprite.y() );
1908 }
1909 else if ( Arena::CASTLE_BOTTOM_GATE_TOWER_POS == cellId ) {
1910 const fheroes2::Sprite & towerSprite = fheroes2::AGG::GetICN( castleIcnId, 17 );
1911 fheroes2::Blit( towerSprite, _mainSurface, 399 + towerSprite.x(), 321 + towerSprite.y() );
1912 }
1913 }
1914
RedrawCastleMainTower(const Castle & castle)1915 void Battle::Interface::RedrawCastleMainTower( const Castle & castle )
1916 {
1917 const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::Get4Castle( castle.GetRace() ), ( Arena::GetTower( TWR_CENTER )->isValid() ? 20 : 26 ) );
1918
1919 fheroes2::Blit( sprite, _mainSurface, sprite.x(), sprite.y() );
1920 }
1921
RedrawLowObjects(s32 cell_index)1922 void Battle::Interface::RedrawLowObjects( s32 cell_index )
1923 {
1924 const Cell * cell = Board::GetCell( cell_index );
1925 if ( cell == nullptr )
1926 return;
1927
1928 const int cellObjectId = cell->GetObject();
1929 if ( cellObjectId == 0 ) {
1930 // No object exists.
1931 return;
1932 }
1933
1934 int objectIcnId = 0;
1935
1936 switch ( cellObjectId ) {
1937 case 0x84:
1938 objectIcnId = ICN::COBJ0004;
1939 break;
1940 case 0x87:
1941 objectIcnId = ICN::COBJ0007;
1942 break;
1943 case 0x90:
1944 objectIcnId = ICN::COBJ0016;
1945 break;
1946 case 0x9E:
1947 objectIcnId = ICN::COBJ0030;
1948 break;
1949 case 0x9F:
1950 objectIcnId = ICN::COBJ0031;
1951 break;
1952 default:
1953 return;
1954 }
1955
1956 const fheroes2::Sprite & objectSprite = fheroes2::AGG::GetICN( objectIcnId, 0 );
1957 const fheroes2::Rect & pt = cell->GetPos();
1958 fheroes2::Blit( objectSprite, _mainSurface, pt.x + pt.width / 2 + objectSprite.x(), pt.y + pt.height + objectSprite.y() + cellYOffset );
1959 }
1960
RedrawHighObjects(s32 cell_index)1961 void Battle::Interface::RedrawHighObjects( s32 cell_index )
1962 {
1963 const Cell * cell = Board::GetCell( cell_index );
1964 if ( cell == nullptr )
1965 return;
1966
1967 const int cellObjectId = cell->GetObject();
1968 if ( cellObjectId == 0 ) {
1969 // No object exists.
1970 return;
1971 }
1972
1973 int objectIcnId = 0;
1974
1975 switch ( cellObjectId ) {
1976 case 0x80:
1977 objectIcnId = ICN::COBJ0000;
1978 break;
1979 case 0x81:
1980 objectIcnId = ICN::COBJ0001;
1981 break;
1982 case 0x82:
1983 objectIcnId = ICN::COBJ0002;
1984 break;
1985 case 0x83:
1986 objectIcnId = ICN::COBJ0003;
1987 break;
1988 case 0x85:
1989 objectIcnId = ICN::COBJ0005;
1990 break;
1991 case 0x86:
1992 objectIcnId = ICN::COBJ0006;
1993 break;
1994 case 0x88:
1995 objectIcnId = ICN::COBJ0008;
1996 break;
1997 case 0x89:
1998 objectIcnId = ICN::COBJ0009;
1999 break;
2000 case 0x8A:
2001 objectIcnId = ICN::COBJ0010;
2002 break;
2003 case 0x8B:
2004 objectIcnId = ICN::COBJ0011;
2005 break;
2006 case 0x8C:
2007 objectIcnId = ICN::COBJ0012;
2008 break;
2009 case 0x8D:
2010 objectIcnId = ICN::COBJ0013;
2011 break;
2012 case 0x8E:
2013 objectIcnId = ICN::COBJ0014;
2014 break;
2015 case 0x8F:
2016 objectIcnId = ICN::COBJ0015;
2017 break;
2018 case 0x91:
2019 objectIcnId = ICN::COBJ0017;
2020 break;
2021 case 0x92:
2022 objectIcnId = ICN::COBJ0018;
2023 break;
2024 case 0x93:
2025 objectIcnId = ICN::COBJ0019;
2026 break;
2027 case 0x94:
2028 objectIcnId = ICN::COBJ0020;
2029 break;
2030 case 0x95:
2031 objectIcnId = ICN::COBJ0021;
2032 break;
2033 case 0x96:
2034 objectIcnId = ICN::COBJ0022;
2035 break;
2036 case 0x97:
2037 objectIcnId = ICN::COBJ0023;
2038 break;
2039 case 0x98:
2040 objectIcnId = ICN::COBJ0024;
2041 break;
2042 case 0x99:
2043 objectIcnId = ICN::COBJ0025;
2044 break;
2045 case 0x9A:
2046 objectIcnId = ICN::COBJ0026;
2047 break;
2048 case 0x9B:
2049 objectIcnId = ICN::COBJ0027;
2050 break;
2051 case 0x9C:
2052 objectIcnId = ICN::COBJ0028;
2053 break;
2054 case 0x9D:
2055 objectIcnId = ICN::COBJ0029;
2056 break;
2057 default:
2058 return;
2059 }
2060
2061 const fheroes2::Sprite & objectSprite = fheroes2::AGG::GetICN( objectIcnId, 0 );
2062 const fheroes2::Rect & pt = cell->GetPos();
2063 fheroes2::Blit( objectSprite, _mainSurface, pt.x + pt.width / 2 + objectSprite.x(), pt.y + pt.height + objectSprite.y() + cellYOffset );
2064 }
2065
RedrawKilled()2066 void Battle::Interface::RedrawKilled()
2067 {
2068 // redraw killed troop
2069 const Indexes cells = arena.GraveyardClosedCells();
2070
2071 for ( Indexes::const_iterator it = cells.begin(); it != cells.end(); ++it ) {
2072 const std::vector<const Unit *> & units = arena.GetGraveyardTroops( *it );
2073 for ( size_t i = 0; i < units.size(); ++i ) {
2074 if ( units[i] && *it != units[i]->GetTailIndex() ) {
2075 RedrawTroopSprite( *units[i] );
2076 }
2077 }
2078 }
2079 }
2080
GetBattleCursor(std::string & statusMsg) const2081 int Battle::Interface::GetBattleCursor( std::string & statusMsg ) const
2082 {
2083 statusMsg.clear();
2084
2085 const Cell * cell = Board::GetCell( index_pos );
2086
2087 if ( cell && _currentUnit ) {
2088 const Unit * b_enemy = cell->GetUnit();
2089
2090 if ( b_enemy ) {
2091 if ( _currentUnit->GetCurrentColor() == b_enemy->GetColor() || ( _currentUnit == b_enemy ) ) {
2092 statusMsg = _( "View %{monster} info" );
2093 StringReplace( statusMsg, "%{monster}", b_enemy->GetMultiName() );
2094 return Cursor::WAR_INFO;
2095 }
2096 else {
2097 if ( _currentUnit->isArchers() && !_currentUnit->isHandFighting() ) {
2098 statusMsg = _( "Shoot %{monster}" );
2099 statusMsg.append( " " );
2100 statusMsg.append( _n( "(1 shot left)", "(%{count} shots left)", _currentUnit->GetShots() ) );
2101 StringReplace( statusMsg, "%{monster}", b_enemy->GetMultiName() );
2102 StringReplace( statusMsg, "%{count}", _currentUnit->GetShots() );
2103
2104 return arena.IsShootingPenalty( *_currentUnit, *b_enemy ) ? Cursor::WAR_BROKENARROW : Cursor::WAR_ARROW;
2105 }
2106 else {
2107 const int dir = cell->GetTriangleDirection( GetMouseCursor() );
2108 const int cursor = GetSwordCursorDirection( dir );
2109
2110 if ( cursor && Board::isValidDirection( index_pos, dir ) ) {
2111 const s32 from = Board::GetIndexDirection( index_pos, dir );
2112
2113 if ( Board::CanAttackUnitFromCell( *_currentUnit, from ) ) {
2114 statusMsg = _( "Attack %{monster}" );
2115 StringReplace( statusMsg, "%{monster}", b_enemy->GetName() );
2116
2117 return cursor;
2118 }
2119 }
2120 }
2121 }
2122 }
2123 else if ( cell->isReachableForHead() || cell->isReachableForTail() ) {
2124 statusMsg = _currentUnit->isFlying() ? _( "Fly %{monster} here" ) : _( "Move %{monster} here" );
2125 StringReplace( statusMsg, "%{monster}", _currentUnit->GetName() );
2126 return _currentUnit->isFlying() ? Cursor::WAR_FLY : Cursor::WAR_MOVE;
2127 }
2128 }
2129
2130 statusMsg = _( "Turn %{turn}" );
2131 StringReplace( statusMsg, "%{turn}", arena.GetCurrentTurn() );
2132
2133 return Cursor::WAR_NONE;
2134 }
2135
GetBattleSpellCursor(std::string & statusMsg) const2136 int Battle::Interface::GetBattleSpellCursor( std::string & statusMsg ) const
2137 {
2138 statusMsg.clear();
2139
2140 const Cell * cell = Board::GetCell( index_pos );
2141 const Spell & spell = humanturn_spell;
2142
2143 if ( cell && _currentUnit && spell.isValid() ) {
2144 const Unit * b_stats = cell->GetUnit();
2145
2146 // over graveyard
2147 if ( !b_stats && arena.GraveyardAllowResurrect( index_pos, spell ) ) {
2148 b_stats = arena.GraveyardLastTroop( index_pos );
2149 if ( b_stats->isWide() ) { // we need to check tail and head positions
2150 const Cell * tailCell = Board::GetCell( b_stats->GetTailIndex() );
2151 const Cell * headCell = Board::GetCell( b_stats->GetHeadIndex() );
2152 if ( !tailCell || tailCell->GetUnit() || !headCell || headCell->GetUnit() )
2153 b_stats = nullptr;
2154 }
2155 }
2156
2157 // teleport check first
2158 if ( Board::isValidIndex( teleport_src ) ) {
2159 const Unit * unitToTeleport = arena.GetTroopBoard( teleport_src );
2160
2161 assert( unitToTeleport != nullptr );
2162
2163 if ( !b_stats && cell->isPassable3( *unitToTeleport, false ) ) {
2164 statusMsg = _( "Teleport here" );
2165 return Cursor::SP_TELEPORT;
2166 }
2167
2168 statusMsg = _( "Invalid teleport destination" );
2169 return Cursor::WAR_NONE;
2170 }
2171 else if ( b_stats && b_stats->AllowApplySpell( spell, _currentUnit->GetCurrentOrArmyCommander() ) ) {
2172 statusMsg = _( "Cast %{spell} on %{monster}" );
2173 StringReplace( statusMsg, "%{spell}", spell.GetName() );
2174 StringReplace( statusMsg, "%{monster}", b_stats->GetName() );
2175 return GetCursorFromSpell( spell.GetID() );
2176 }
2177 else if ( !spell.isApplyToFriends() && !spell.isApplyToEnemies() && !spell.isApplyToAnyTroops() ) {
2178 statusMsg = _( "Cast %{spell}" );
2179 StringReplace( statusMsg, "%{spell}", spell.GetName() );
2180 return GetCursorFromSpell( spell.GetID() );
2181 }
2182 }
2183
2184 statusMsg = _( "Select spell target" );
2185
2186 return Cursor::WAR_NONE;
2187 }
2188
HumanTurn(const Unit & b,Actions & a)2189 void Battle::Interface::HumanTurn( const Unit & b, Actions & a )
2190 {
2191 Cursor & cursor = Cursor::Get();
2192 LocalEvent & le = LocalEvent::Get();
2193
2194 cursor.SetThemes( Cursor::WAR_POINTER );
2195 _currentUnit = &b;
2196 humanturn_redraw = false;
2197 humanturn_exit = false;
2198 catapult_frame = 0;
2199
2200 // in case we moved the window
2201 _interfacePosition = border.GetArea();
2202
2203 Board & board = *Arena::GetBoard();
2204 board.Reset();
2205 board.SetScanPassability( b );
2206
2207 popup.Reset();
2208
2209 // safe position coord
2210 CursorPosition cursorPosition;
2211
2212 Redraw();
2213
2214 std::string msg;
2215 animation_flags_frame = 0;
2216
2217 ResetIdleTroopAnimation();
2218
2219 while ( !humanturn_exit && le.HandleEvents() ) {
2220 // move cursor
2221 int32_t indexNew = -1;
2222 if ( le.MouseCursor( fheroes2::Rect( _interfacePosition.x, _interfacePosition.y, _interfacePosition.width, _interfacePosition.height - status.height ) ) ) {
2223 indexNew = board.GetIndexAbsPosition( GetMouseCursor() );
2224 }
2225 if ( index_pos != indexNew ) {
2226 index_pos = indexNew;
2227 humanturn_redraw = true;
2228 }
2229
2230 if ( humanturn_spell.isValid() )
2231 HumanCastSpellTurn( b, a, msg );
2232 else
2233 HumanBattleTurn( b, a, msg );
2234
2235 if ( humanturn_exit )
2236 cursor.SetThemes( Cursor::WAIT );
2237
2238 // update status
2239 if ( msg != status.GetMessage() ) {
2240 status.SetMessage( msg );
2241 humanturn_redraw = true;
2242 }
2243
2244 // animation troops
2245 if ( IdleTroopsAnimation() )
2246 humanturn_redraw = true;
2247
2248 CheckGlobalEvents( le );
2249
2250 // redraw arena
2251 if ( humanturn_redraw ) {
2252 Redraw();
2253 humanturn_redraw = false;
2254 }
2255 }
2256
2257 popup.Reset();
2258 _currentUnit = nullptr;
2259 }
2260
HumanBattleTurn(const Unit & b,Actions & a,std::string & msg)2261 void Battle::Interface::HumanBattleTurn( const Unit & b, Actions & a, std::string & msg )
2262 {
2263 Cursor & cursor = Cursor::Get();
2264 LocalEvent & le = LocalEvent::Get();
2265 const Settings & conf = Settings::Get();
2266
2267 if ( le.KeyPress() ) {
2268 // skip
2269 if ( Game::HotKeyPressEvent( Game::EVENT_BATTLE_HARDSKIP ) ) {
2270 a.push_back( Command( CommandType::MSG_BATTLE_SKIP, b.GetUID(), true ) );
2271 humanturn_exit = true;
2272 }
2273 else
2274 // soft skip
2275 if ( Game::HotKeyPressEvent( Game::EVENT_BATTLE_SOFTSKIP ) ) {
2276 a.push_back( Command( CommandType::MSG_BATTLE_SKIP, b.GetUID(), !conf.ExtBattleSoftWait() ) );
2277 humanturn_exit = true;
2278 }
2279 else
2280 // options
2281 if ( Game::HotKeyPressEvent( Game::EVENT_BATTLE_OPTIONS ) )
2282 EventShowOptions();
2283 else
2284 // auto switch
2285 if ( Game::HotKeyPressEvent( Game::EVENT_BATTLE_AUTOSWITCH ) )
2286 EventAutoSwitch( b, a );
2287 else
2288 // cast
2289 if ( Game::HotKeyPressEvent( Game::EVENT_BATTLE_CASTSPELL ) )
2290 ProcessingHeroDialogResult( 1, a );
2291 else
2292 // retreat
2293 if ( Game::HotKeyPressEvent( Game::EVENT_BATTLE_RETREAT ) )
2294 ProcessingHeroDialogResult( 2, a );
2295 else
2296 // surrender
2297 if ( Game::HotKeyPressEvent( Game::EVENT_BATTLE_SURRENDER ) )
2298 ProcessingHeroDialogResult( 3, a );
2299
2300 // debug only
2301 #ifdef WITH_DEBUG
2302 if ( IS_DEVEL() )
2303 switch ( le.KeyValue() ) {
2304 case KEY_w:
2305 // fast wins game
2306 arena.GetResult().army1 = RESULT_WINS;
2307 humanturn_exit = true;
2308 a.push_back( Command( CommandType::MSG_BATTLE_END_TURN, b.GetUID() ) );
2309 break;
2310
2311 case KEY_l:
2312 // fast loss game
2313 arena.GetResult().army1 = RESULT_LOSS;
2314 humanturn_exit = true;
2315 a.push_back( Command( CommandType::MSG_BATTLE_END_TURN, b.GetUID() ) );
2316 break;
2317
2318 default:
2319 break;
2320 }
2321 #endif
2322 }
2323
2324 // Add offsets to inner objects
2325 const fheroes2::Rect mainTowerRect = main_tower + _interfacePosition.getPosition();
2326 const fheroes2::Rect armiesOrderRect = armies_order + _interfacePosition.getPosition();
2327 if ( Arena::GetTower( TWR_CENTER ) && le.MouseCursor( mainTowerRect ) ) {
2328 cursor.SetThemes( Cursor::WAR_INFO );
2329 msg = _( "View Ballista info" );
2330
2331 if ( le.MouseClickLeft( mainTowerRect ) || le.MousePressRight( mainTowerRect ) ) {
2332 const Castle * cstl = Arena::GetCastle();
2333 std::string ballistaMessage = Tower::GetInfo( *cstl );
2334
2335 if ( cstl->isBuild( BUILD_MOAT ) ) {
2336 ballistaMessage.append( "\n \n" );
2337 ballistaMessage.append( Battle::Board::GetMoatInfo() );
2338 }
2339
2340 Dialog::Message( _( "Ballista" ), ballistaMessage, Font::BIG, le.MousePressRight() ? 0 : Dialog::OK );
2341 }
2342 }
2343 else if ( conf.ExtBattleShowBattleOrder() && le.MouseCursor( armiesOrderRect ) ) {
2344 cursor.SetThemes( Cursor::POINTER );
2345 armies_order.QueueEventProcessing( msg, _interfacePosition.getPosition() );
2346 }
2347 else if ( le.MouseCursor( btn_auto.area() ) ) {
2348 cursor.SetThemes( Cursor::WAR_POINTER );
2349 msg = _( "Enable auto combat" );
2350 ButtonAutoAction( b, a );
2351
2352 if ( le.MousePressRight() ) {
2353 Dialog::Message( _( "Auto Combat" ), _( "Allows the computer to fight out the battle for you." ), Font::BIG );
2354 }
2355 }
2356 else if ( le.MouseCursor( btn_settings.area() ) ) {
2357 cursor.SetThemes( Cursor::WAR_POINTER );
2358 msg = _( "Customize system options" );
2359 ButtonSettingsAction();
2360
2361 if ( le.MousePressRight() ) {
2362 Dialog::Message( _( "System Options" ), _( "Allows you to customize the combat screen." ), Font::BIG );
2363 }
2364 }
2365 else if ( conf.ExtBattleSoftWait() && le.MouseCursor( btn_wait.area() ) ) {
2366 cursor.SetThemes( Cursor::WAR_POINTER );
2367 msg = _( "Wait this unit" );
2368 ButtonWaitAction( a );
2369
2370 if ( le.MousePressRight() ) {
2371 Dialog::Message( _( "Wait" ), _( "Waits the current creature. The current creature delays its turn until after all other creatures have had their turn." ),
2372 Font::BIG );
2373 }
2374 }
2375 else if ( le.MouseCursor( btn_skip.area() ) ) {
2376 cursor.SetThemes( Cursor::WAR_POINTER );
2377 msg = _( "Skip this unit" );
2378 ButtonSkipAction( a );
2379
2380 if ( le.MousePressRight() ) {
2381 Dialog::Message( _( "Skip" ), _( "Skips the current creature. The current creature ends its turn and does not get to go again until the next round." ),
2382 Font::BIG );
2383 }
2384 }
2385 else if ( opponent1 && le.MouseCursor( opponent1->GetArea() + _interfacePosition.getPosition() ) ) {
2386 const fheroes2::Rect opponent1Area = opponent1->GetArea() + _interfacePosition.getPosition();
2387 if ( arena.GetCurrentColor() == arena.GetArmyColor1() ) {
2388 msg = _( "View Hero's options" );
2389 cursor.SetThemes( Cursor::WAR_HERO );
2390
2391 if ( le.MouseClickLeft( opponent1Area ) ) {
2392 ProcessingHeroDialogResult( arena.DialogBattleHero( *opponent1->GetHero(), true, status ), a );
2393 humanturn_redraw = true;
2394 }
2395 }
2396 else {
2397 msg = _( "View opposing Hero" );
2398 cursor.SetThemes( Cursor::WAR_INFO );
2399
2400 if ( le.MouseClickLeft( opponent1Area ) ) {
2401 arena.DialogBattleHero( *opponent1->GetHero(), true, status );
2402 humanturn_redraw = true;
2403 }
2404 }
2405
2406 if ( le.MousePressRight( opponent1Area ) ) {
2407 arena.DialogBattleHero( *opponent1->GetHero(), false, status );
2408 humanturn_redraw = true;
2409 }
2410 }
2411 else if ( opponent2 && le.MouseCursor( opponent2->GetArea() + _interfacePosition.getPosition() ) ) {
2412 const fheroes2::Rect opponent2Area = opponent2->GetArea() + _interfacePosition.getPosition();
2413 if ( arena.GetCurrentColor() == arena.GetForce2().GetColor() ) {
2414 msg = _( "View Hero's options" );
2415 cursor.SetThemes( Cursor::WAR_HERO );
2416
2417 if ( le.MouseClickLeft( opponent2Area ) ) {
2418 ProcessingHeroDialogResult( arena.DialogBattleHero( *opponent2->GetHero(), true, status ), a );
2419 humanturn_redraw = true;
2420 }
2421 }
2422 else {
2423 msg = _( "View opposing Hero" );
2424 cursor.SetThemes( Cursor::WAR_INFO );
2425
2426 if ( le.MouseClickLeft( opponent2Area ) ) {
2427 arena.DialogBattleHero( *opponent2->GetHero(), true, status );
2428 humanturn_redraw = true;
2429 }
2430 }
2431
2432 if ( le.MousePressRight( opponent2Area ) ) {
2433 arena.DialogBattleHero( *opponent2->GetHero(), false, status );
2434 humanturn_redraw = true;
2435 }
2436 }
2437 else if ( listlog && listlog->isOpenLog() && le.MouseCursor( listlog->GetArea() ) ) {
2438 cursor.SetThemes( Cursor::WAR_POINTER );
2439
2440 listlog->QueueEventProcessing();
2441 }
2442 else if ( le.MouseCursor( fheroes2::Rect( _interfacePosition.x, _interfacePosition.y, _interfacePosition.width, _interfacePosition.height - status.height ) ) ) {
2443 const int themes = GetBattleCursor( msg );
2444
2445 if ( cursor.Themes() != themes )
2446 cursor.SetThemes( themes );
2447
2448 const Cell * cell = Board::GetCell( index_pos );
2449
2450 if ( cell ) {
2451 if ( CursorAttack( themes ) ) {
2452 const Unit * b_enemy = cell->GetUnit();
2453 popup.SetInfo( cell, _currentUnit, b_enemy, _interfacePosition.getPosition() );
2454 }
2455 else
2456 popup.Reset();
2457
2458 if ( le.MouseClickLeft() )
2459 MouseLeftClickBoardAction( themes, *cell, a );
2460 else if ( le.MousePressRight() )
2461 MousePressRightBoardAction( themes, *cell );
2462 }
2463 else {
2464 le.MouseClickLeft();
2465 le.MousePressRight();
2466 }
2467 }
2468 else if ( le.MouseCursor( status ) ) {
2469 if ( listlog ) {
2470 msg = ( listlog->isOpenLog() ? _( "Hide logs" ) : _( "Show logs" ) );
2471 if ( le.MouseClickLeft( status ) )
2472 listlog->SetOpenLog( !listlog->isOpenLog() );
2473 }
2474 cursor.SetThemes( Cursor::WAR_POINTER );
2475 }
2476 else {
2477 cursor.SetThemes( Cursor::WAR_NONE );
2478 le.MouseClickLeft();
2479 le.MousePressRight();
2480 }
2481 }
2482
HumanCastSpellTurn(const Unit &,Actions & a,std::string & msg)2483 void Battle::Interface::HumanCastSpellTurn( const Unit & /*b*/, Actions & a, std::string & msg )
2484 {
2485 Cursor & cursor = Cursor::Get();
2486 LocalEvent & le = LocalEvent::Get();
2487
2488 // reset cast
2489 if ( le.MousePressRight() || Game::HotKeyPressEvent( Game::EVENT_DEFAULT_EXIT ) ) {
2490 humanturn_spell = Spell::NONE;
2491 teleport_src = -1;
2492 }
2493 else if ( le.MouseCursor( _interfacePosition ) && humanturn_spell.isValid() ) {
2494 const int themes = GetBattleSpellCursor( msg );
2495
2496 if ( cursor.Themes() != themes )
2497 cursor.SetThemes( themes );
2498
2499 if ( le.MouseClickLeft() && Cursor::WAR_NONE != cursor.Themes() ) {
2500 if ( !Board::isValidIndex( index_pos ) ) {
2501 DEBUG_LOG( DBG_BATTLE, DBG_WARN,
2502 "dst: "
2503 << "out of range" );
2504 return;
2505 }
2506
2507 if ( listlog ) {
2508 std::string str = _( "%{color} casts a spell: %{spell}" );
2509 const HeroBase * current_commander = arena.GetCurrentCommander();
2510 if ( current_commander )
2511 StringReplace( str, "%{color}", Color::String( current_commander->GetColor() ) );
2512 StringReplace( str, "%{spell}", humanturn_spell.GetName() );
2513 listlog->AddMessage( str );
2514 }
2515
2516 DEBUG_LOG( DBG_BATTLE, DBG_TRACE, humanturn_spell.GetName() << ", dst: " << index_pos );
2517
2518 if ( Cursor::SP_TELEPORT == cursor.Themes() ) {
2519 if ( 0 > teleport_src )
2520 teleport_src = index_pos;
2521 else {
2522 a.push_back( Command( CommandType::MSG_BATTLE_CAST, Spell::TELEPORT, teleport_src, index_pos ) );
2523 humanturn_spell = Spell::NONE;
2524 humanturn_exit = true;
2525 teleport_src = -1;
2526 }
2527 }
2528 else if ( Cursor::SP_MIRRORIMAGE == cursor.Themes() ) {
2529 a.push_back( Command( CommandType::MSG_BATTLE_CAST, Spell::MIRRORIMAGE, index_pos ) );
2530 humanturn_spell = Spell::NONE;
2531 humanturn_exit = true;
2532 }
2533 else {
2534 a.push_back( Command( CommandType::MSG_BATTLE_CAST, humanturn_spell.GetID(), index_pos ) );
2535 humanturn_spell = Spell::NONE;
2536 humanturn_exit = true;
2537 }
2538 }
2539 }
2540 else {
2541 cursor.SetThemes( Cursor::WAR_NONE );
2542 }
2543 }
2544
FadeArena(bool clearMessageLog)2545 void Battle::Interface::FadeArena( bool clearMessageLog )
2546 {
2547 AGG::ResetMixer();
2548
2549 if ( clearMessageLog ) {
2550 status.clear();
2551 status.Redraw();
2552 }
2553
2554 Redraw();
2555
2556 const fheroes2::Rect srt = border.GetArea();
2557 fheroes2::Image top( srt.width, srt.height );
2558 fheroes2::Display & display = fheroes2::Display::instance();
2559
2560 fheroes2::Copy( display, srt.x, srt.y, top, 0, 0, srt.width, srt.height );
2561 fheroes2::FadeDisplayWithPalette( top, srt.getPosition(), 5, 300, 5 );
2562
2563 display.render();
2564 }
2565
GetIndexIndicator(const Unit & b)2566 int Battle::GetIndexIndicator( const Unit & b )
2567 {
2568 // yellow
2569 if ( b.Modes( IS_GREEN_STATUS ) && b.Modes( IS_RED_STATUS ) )
2570 return 13;
2571 else
2572 // green
2573 if ( b.Modes( IS_GREEN_STATUS ) )
2574 return 12;
2575 else
2576 // red
2577 if ( b.Modes( IS_RED_STATUS ) )
2578 return 14;
2579
2580 return 10;
2581 }
2582
EventShowOptions(void)2583 void Battle::Interface::EventShowOptions( void )
2584 {
2585 btn_settings.drawOnPress();
2586 DialogBattleSettings();
2587 btn_settings.drawOnRelease();
2588 humanturn_redraw = true;
2589 }
2590
EventAutoSwitch(const Unit & b,Actions & a)2591 void Battle::Interface::EventAutoSwitch( const Unit & b, Actions & a )
2592 {
2593 btn_auto.drawOnPress();
2594
2595 a.push_back( Command( CommandType::MSG_BATTLE_AUTO, b.GetColor() ) );
2596
2597 Cursor::Get().SetThemes( Cursor::WAIT );
2598 humanturn_redraw = true;
2599 humanturn_exit = true;
2600
2601 btn_auto.drawOnRelease();
2602 }
2603
ButtonAutoAction(const Unit & b,Actions & a)2604 void Battle::Interface::ButtonAutoAction( const Unit & b, Actions & a )
2605 {
2606 LocalEvent & le = LocalEvent::Get();
2607
2608 le.MousePressLeft( btn_auto.area() ) ? btn_auto.drawOnPress() : btn_auto.drawOnRelease();
2609
2610 if ( le.MouseClickLeft( btn_auto.area() ) )
2611 EventAutoSwitch( b, a );
2612 }
2613
ButtonSettingsAction(void)2614 void Battle::Interface::ButtonSettingsAction( void )
2615 {
2616 LocalEvent & le = LocalEvent::Get();
2617
2618 le.MousePressLeft( btn_settings.area() ) ? btn_settings.drawOnPress() : btn_settings.drawOnRelease();
2619
2620 if ( le.MouseClickLeft( btn_settings.area() ) ) {
2621 DialogBattleSettings();
2622 humanturn_redraw = true;
2623 }
2624 }
2625
ButtonWaitAction(Actions & a)2626 void Battle::Interface::ButtonWaitAction( Actions & a )
2627 {
2628 LocalEvent & le = LocalEvent::Get();
2629
2630 le.MousePressLeft( btn_wait.area() ) ? btn_wait.drawOnPress() : btn_wait.drawOnRelease();
2631
2632 if ( le.MouseClickLeft( btn_wait.area() ) && _currentUnit ) {
2633 a.push_back( Command( CommandType::MSG_BATTLE_SKIP, _currentUnit->GetUID(), false ) );
2634 humanturn_exit = true;
2635 }
2636 }
2637
ButtonSkipAction(Actions & a)2638 void Battle::Interface::ButtonSkipAction( Actions & a )
2639 {
2640 LocalEvent & le = LocalEvent::Get();
2641
2642 le.MousePressLeft( btn_skip.area() ) ? btn_skip.drawOnPress() : btn_skip.drawOnRelease();
2643
2644 if ( le.MouseClickLeft( btn_skip.area() ) && _currentUnit ) {
2645 a.push_back( Command( CommandType::MSG_BATTLE_SKIP, _currentUnit->GetUID(), true ) );
2646 humanturn_exit = true;
2647 }
2648 }
2649
MousePressRightBoardAction(u32,const Cell & cell) const2650 void Battle::Interface::MousePressRightBoardAction( u32 /*themes*/, const Cell & cell ) const
2651 {
2652 const Unit * b = cell.GetUnit();
2653
2654 if ( b ) {
2655 Dialog::ArmyInfo( *b, Dialog::READONLY, b->isReflect() );
2656 }
2657 }
2658
MouseLeftClickBoardAction(u32 themes,const Cell & cell,Actions & a)2659 void Battle::Interface::MouseLeftClickBoardAction( u32 themes, const Cell & cell, Actions & a )
2660 {
2661 const int32_t index = cell.GetIndex();
2662 const Unit * b = cell.GetUnit();
2663
2664 if ( _currentUnit ) {
2665 switch ( themes ) {
2666 case Cursor::WAR_FLY:
2667 case Cursor::WAR_MOVE:
2668 a.push_back( Command( CommandType::MSG_BATTLE_MOVE, _currentUnit->GetUID(), Board::FixupDestinationCell( *_currentUnit, index ) ) );
2669 a.push_back( Command( CommandType::MSG_BATTLE_END_TURN, _currentUnit->GetUID() ) );
2670 humanturn_exit = true;
2671 break;
2672
2673 case Cursor::SWORD_TOPLEFT:
2674 case Cursor::SWORD_TOPRIGHT:
2675 case Cursor::SWORD_RIGHT:
2676 case Cursor::SWORD_BOTTOMRIGHT:
2677 case Cursor::SWORD_BOTTOMLEFT:
2678 case Cursor::SWORD_LEFT: {
2679 const Unit * enemy = b;
2680 const int dir = GetDirectionFromCursorSword( themes );
2681
2682 if ( enemy && Board::isValidDirection( index, dir ) ) {
2683 const int32_t move = Board::FixupDestinationCell( *_currentUnit, Board::GetIndexDirection( index, dir ) );
2684
2685 if ( _currentUnit->GetHeadIndex() != move ) {
2686 a.push_back( Command( CommandType::MSG_BATTLE_MOVE, _currentUnit->GetUID(), move ) );
2687 }
2688 a.push_back( Command( CommandType::MSG_BATTLE_ATTACK, _currentUnit->GetUID(), enemy->GetUID(), index, Board::GetReflectDirection( dir ) ) );
2689 a.push_back( Command( CommandType::MSG_BATTLE_END_TURN, _currentUnit->GetUID() ) );
2690 humanturn_exit = true;
2691 }
2692 break;
2693 }
2694
2695 case Cursor::WAR_BROKENARROW:
2696 case Cursor::WAR_ARROW: {
2697 const Unit * enemy = b;
2698
2699 if ( enemy ) {
2700 a.push_back( Command( CommandType::MSG_BATTLE_ATTACK, _currentUnit->GetUID(), enemy->GetUID(), index, 0 ) );
2701 a.push_back( Command( CommandType::MSG_BATTLE_END_TURN, _currentUnit->GetUID() ) );
2702 humanturn_exit = true;
2703 }
2704 break;
2705 }
2706
2707 case Cursor::WAR_INFO: {
2708 if ( b ) {
2709 Dialog::ArmyInfo( *b, Dialog::BUTTONS | Dialog::READONLY, b->isReflect() );
2710 humanturn_redraw = true;
2711 }
2712 break;
2713 }
2714
2715 default:
2716 break;
2717 }
2718 }
2719 }
2720
AnimateUnitWithDelay(Unit & unit,uint32_t delay)2721 void Battle::Interface::AnimateUnitWithDelay( Unit & unit, uint32_t delay )
2722 {
2723 if ( unit.isFinishAnimFrame() ) // nothing to animate
2724 return;
2725
2726 LocalEvent & le = LocalEvent::Get();
2727 const uint64_t frameDelay = ( unit.animation.animationLength() > 0 ) ? delay / unit.animation.animationLength() : 0;
2728
2729 while ( le.HandleEvents( false ) ) {
2730 CheckGlobalEvents( le );
2731
2732 if ( Game::validateCustomAnimationDelay( frameDelay ) ) {
2733 Redraw();
2734 if ( unit.isFinishAnimFrame() )
2735 break;
2736 unit.IncreaseAnimFrame();
2737 }
2738 }
2739 }
2740
AnimateOpponents(OpponentSprite * target)2741 void Battle::Interface::AnimateOpponents( OpponentSprite * target )
2742 {
2743 if ( target == nullptr ) // nothing to animate
2744 return;
2745
2746 LocalEvent & le = LocalEvent::Get();
2747 while ( le.HandleEvents() && !target->isFinishFrame() ) {
2748 if ( Game::validateAnimationDelay( Game::BATTLE_OPPONENTS_DELAY ) ) {
2749 target->IncreaseAnimFrame();
2750 Redraw();
2751 }
2752 }
2753 }
2754
RedrawTroopDefaultDelay(Unit & unit)2755 void Battle::Interface::RedrawTroopDefaultDelay( Unit & unit )
2756 {
2757 if ( unit.isFinishAnimFrame() ) // nothing to animate
2758 return;
2759
2760 LocalEvent & le = LocalEvent::Get();
2761
2762 while ( le.HandleEvents( false ) ) {
2763 CheckGlobalEvents( le );
2764
2765 if ( Game::validateAnimationDelay( Game::BATTLE_FRAME_DELAY ) ) {
2766 Redraw();
2767 if ( unit.isFinishAnimFrame() )
2768 break;
2769 unit.IncreaseAnimFrame();
2770 }
2771 }
2772 }
2773
RedrawActionSkipStatus(const Unit & attacker)2774 void Battle::Interface::RedrawActionSkipStatus( const Unit & attacker )
2775 {
2776 std::string msg;
2777 if ( attacker.Modes( TR_HARDSKIP ) ) {
2778 msg = _( "%{name} skip their turn." );
2779 }
2780 else {
2781 msg = _( "%{name} wait their turn." );
2782 }
2783
2784 StringReplace( msg, "%{name}", attacker.GetName() );
2785 status.SetMessage( msg, true );
2786 }
2787
RedrawMissileAnimation(const fheroes2::Point & startPos,const fheroes2::Point & endPos,double angle,uint32_t monsterID)2788 void Battle::Interface::RedrawMissileAnimation( const fheroes2::Point & startPos, const fheroes2::Point & endPos, double angle, uint32_t monsterID )
2789 {
2790 LocalEvent & le = LocalEvent::Get();
2791 fheroes2::Sprite missile;
2792
2793 const bool reverse = startPos.x > endPos.x;
2794 const bool isMage = ( monsterID == Monster::MAGE || monsterID == Monster::ARCHMAGE );
2795
2796 // Mage is channeling the bolt; doesn't have missile sprite
2797 if ( isMage )
2798 fheroes2::delayforMs( Game::ApplyBattleSpeed( 115 ) );
2799 else
2800 missile = fheroes2::AGG::GetICN( Monster::GetMissileICN( monsterID ), static_cast<uint32_t>( Bin_Info::GetMonsterInfo( monsterID ).getProjectileID( angle ) ) );
2801
2802 // Lich/Power lich has projectile speed of 25
2803 const std::vector<fheroes2::Point> points = GetEuclideanLine( startPos, endPos, isMage ? 50 : std::max( missile.width(), 25 ) );
2804 std::vector<fheroes2::Point>::const_iterator pnt = points.begin();
2805
2806 // convert the following code into a function/event service
2807 while ( le.HandleEvents( false ) && pnt != points.end() ) {
2808 CheckGlobalEvents( le );
2809
2810 if ( Game::validateAnimationDelay( Game::BATTLE_MISSILE_DELAY ) ) {
2811 RedrawPartialStart();
2812 if ( isMage ) {
2813 fheroes2::DrawLine( _mainSurface, fheroes2::Point( startPos.x, startPos.y - 2 ), fheroes2::Point( pnt->x, pnt->y - 2 ), 0x77 );
2814 fheroes2::DrawLine( _mainSurface, fheroes2::Point( startPos.x, startPos.y - 1 ), fheroes2::Point( pnt->x, pnt->y - 1 ), 0xB5 );
2815 fheroes2::DrawLine( _mainSurface, startPos, *pnt, 0xBC );
2816 fheroes2::DrawLine( _mainSurface, fheroes2::Point( startPos.x, startPos.y + 1 ), fheroes2::Point( pnt->x, pnt->y + 1 ), 0xB5 );
2817 fheroes2::DrawLine( _mainSurface, fheroes2::Point( startPos.x, startPos.y + 2 ), fheroes2::Point( pnt->x, pnt->y + 2 ), 0x77 );
2818 }
2819 else {
2820 fheroes2::Blit( missile, _mainSurface, reverse ? pnt->x - missile.width() : pnt->x, ( angle > 0 ) ? pnt->y - missile.height() : pnt->y, reverse );
2821 }
2822 RedrawPartialFinish();
2823 ++pnt;
2824 }
2825 }
2826 }
2827
RedrawActionNewTurn() const2828 void Battle::Interface::RedrawActionNewTurn() const
2829 {
2830 if ( !Music::isPlaying() ) {
2831 AGG::PlayMusic( MUS::GetBattleRandom(), true, true );
2832 }
2833
2834 if ( listlog == nullptr ) {
2835 return;
2836 }
2837
2838 std::string msg = _( "Turn %{turn}" );
2839 StringReplace( msg, "%{turn}", arena.GetCurrentTurn() );
2840
2841 listlog->AddMessage( msg );
2842 }
2843
RedrawActionAttackPart1(Unit & attacker,Unit & defender,const TargetsInfo & targets)2844 void Battle::Interface::RedrawActionAttackPart1( Unit & attacker, Unit & defender, const TargetsInfo & targets )
2845 {
2846 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
2847
2848 _currentUnit = nullptr;
2849 _movingUnit = &attacker;
2850 _movingPos = attacker.GetRectPosition().getPosition();
2851
2852 // Unit 'Position' is position of the tile he's standing at
2853 const fheroes2::Rect & pos1 = attacker.GetRectPosition();
2854 const fheroes2::Rect & pos2 = defender.GetRectPosition();
2855
2856 const bool archer = attacker.isArchers() && !attacker.isHandFighting();
2857 const bool isDoubleCell = attacker.isDoubleCellAttack() && 2 == targets.size();
2858
2859 // redraw luck animation
2860 if ( attacker.Modes( LUCK_GOOD | LUCK_BAD ) )
2861 RedrawActionLuck( attacker );
2862
2863 AGG::PlaySound( attacker.M82Attk() );
2864
2865 // long distance attack animation
2866 if ( archer ) {
2867 const fheroes2::Sprite & attackerSprite = fheroes2::AGG::GetICN( attacker.GetMonsterSprite(), attacker.GetFrame() );
2868 const fheroes2::Point attackerPos = GetTroopPosition( attacker, attackerSprite );
2869
2870 // For shooter position we need bottom center position of rear tile
2871 // Use cell coordinates for X because sprite width is very inconsistent (e.g. halfling)
2872 const int rearCenterX = ( attacker.isWide() && attacker.isReflect() ) ? pos1.width * 3 / 4 : CELLW / 2;
2873 const fheroes2::Point shooterPos( pos1.x + rearCenterX, attackerPos.y - attackerSprite.y() );
2874
2875 // Use the front one to calculate the angle, then overwrite
2876 fheroes2::Point offset = attacker.GetStartMissileOffset( Monster_Info::FRONT );
2877
2878 const fheroes2::Point targetPos = defender.GetCenterPoint();
2879
2880 double angle = GetAngle( fheroes2::Point( shooterPos.x + offset.x, shooterPos.y + offset.y ), targetPos );
2881 // This check is made only for situations when a target stands on the same row as shooter.
2882 if ( attacker.GetHeadIndex() / ARENAW == defender.GetHeadIndex() / ARENAW ) {
2883 angle = 0;
2884 }
2885
2886 // Angles are used in Heroes2 as 90 (TOP) -> 0 (FRONT) -> -90 (BOT) degrees
2887 const int direction = angle >= 25.0 ? Monster_Info::TOP : ( angle <= -25.0 ) ? Monster_Info::BOTTOM : Monster_Info::FRONT;
2888
2889 if ( direction != Monster_Info::FRONT )
2890 offset = attacker.GetStartMissileOffset( direction );
2891
2892 // redraw archer attack animation
2893 if ( attacker.SwitchAnimation( Monster_Info::RANG_TOP + direction * 2 ) ) {
2894 AnimateUnitWithDelay( attacker, Game::ApplyBattleSpeed( attacker.animation.getShootingSpeed() ) );
2895 }
2896
2897 const fheroes2::Point missileStart = fheroes2::Point( shooterPos.x + ( attacker.isReflect() ? -offset.x : offset.x ), shooterPos.y + offset.y );
2898
2899 // draw missile animation
2900 RedrawMissileAnimation( missileStart, targetPos, angle, attacker.GetID() );
2901 }
2902 else {
2903 int attackAnim = isDoubleCell ? Monster_Info::RANG_FRONT : Monster_Info::MELEE_FRONT;
2904 if ( pos2.y < pos1.y ) {
2905 attackAnim -= 2;
2906 }
2907 else if ( pos2.y > pos1.y ) {
2908 attackAnim += 2;
2909 }
2910
2911 // redraw melee attack animation
2912 if ( attacker.SwitchAnimation( attackAnim ) ) {
2913 RedrawTroopDefaultDelay( attacker );
2914 }
2915 }
2916
2917 if ( attacker.isAbilityPresent( fheroes2::MonsterAbilityType::AREA_SHOT ) && archer ) {
2918 // Lich cloud animation.
2919 RedrawTroopWithFrameAnimation( defender, ICN::LICHCLOD, attacker.M82Expl(), NONE );
2920 }
2921 }
2922
RedrawActionAttackPart2(Unit & attacker,TargetsInfo & targets)2923 void Battle::Interface::RedrawActionAttackPart2( Unit & attacker, TargetsInfo & targets )
2924 {
2925 // post attack animation
2926 int attackStart = attacker.animation.getCurrentState();
2927 if ( attackStart >= Monster_Info::MELEE_TOP && attackStart <= Monster_Info::RANG_BOT ) {
2928 ++attackStart;
2929 attacker.SwitchAnimation( attackStart );
2930 }
2931
2932 // targets damage animation
2933 RedrawActionWincesKills( targets, &attacker );
2934 RedrawTroopDefaultDelay( attacker );
2935
2936 attacker.SwitchAnimation( Monster_Info::STATIC );
2937
2938 const bool isMirror = targets.size() == 1 && targets.front().defender->isModes( CAP_MIRRORIMAGE );
2939 // draw status for first defender
2940 if ( !isMirror && !targets.empty() ) {
2941 std::string msg = _( "%{attacker} do %{damage} damage." );
2942 StringReplace( msg, "%{attacker}", attacker.GetName() );
2943
2944 if ( 1 < targets.size() ) {
2945 u32 killed = 0;
2946 u32 damage = 0;
2947
2948 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it ) {
2949 if ( !it->defender->isModes( CAP_MIRRORIMAGE ) ) {
2950 killed += ( *it ).killed;
2951 damage += ( *it ).damage;
2952 }
2953 }
2954
2955 StringReplace( msg, "%{damage}", damage );
2956
2957 if ( killed ) {
2958 msg.append( " " );
2959 msg.append( _n( "1 creature perishes.", "%{count} creatures perish.", killed ) );
2960 StringReplace( msg, "%{count}", killed );
2961 }
2962 }
2963 else {
2964 const TargetInfo & target = targets.front();
2965 StringReplace( msg, "%{damage}", target.damage );
2966
2967 if ( target.killed ) {
2968 msg.append( " " );
2969 msg.append( _n( "1 %{defender} perishes.", "%{count} %{defender} perish.", target.killed ) );
2970 StringReplace( msg, "%{count}", target.killed );
2971 StringReplace( msg, "%{defender}", target.defender->GetPluralName( target.killed ) );
2972 }
2973 }
2974
2975 status.SetMessage( msg, true );
2976 status.SetMessage( "", false );
2977 }
2978
2979 _movingUnit = nullptr;
2980 }
2981
RedrawActionWincesKills(TargetsInfo & targets,Unit * attacker)2982 void Battle::Interface::RedrawActionWincesKills( TargetsInfo & targets, Unit * attacker )
2983 {
2984 LocalEvent & le = LocalEvent::Get();
2985
2986 // targets damage animation
2987 int finish = 0;
2988 int deathColor = Color::UNUSED;
2989
2990 std::vector<Unit *> mirrorImages;
2991 std::set<Unit *> resistantTarget;
2992
2993 for ( TargetsInfo::iterator it = targets.begin(); it != targets.end(); ++it ) {
2994 Unit * defender = it->defender;
2995 if ( defender == nullptr ) {
2996 continue;
2997 }
2998
2999 if ( defender->isModes( CAP_MIRRORIMAGE ) )
3000 mirrorImages.push_back( defender );
3001
3002 // kill animation
3003 if ( !defender->isValid() ) {
3004 // destroy linked mirror
3005 if ( defender->isModes( CAP_MIRROROWNER ) )
3006 mirrorImages.push_back( defender->GetMirror() );
3007
3008 defender->SwitchAnimation( Monster_Info::KILL );
3009 AGG::PlaySound( defender->M82Kill() );
3010 ++finish;
3011
3012 deathColor = defender->GetArmyColor();
3013 }
3014 else if ( it->damage ) {
3015 // wince animation
3016 defender->SwitchAnimation( Monster_Info::WNCE );
3017 AGG::PlaySound( defender->M82Wnce() );
3018 ++finish;
3019 }
3020 else {
3021 // have immunity
3022 resistantTarget.insert( it->defender );
3023 AGG::PlaySound( M82::RSBRYFZL );
3024 }
3025 }
3026
3027 if ( deathColor != Color::UNUSED ) {
3028 const bool attackersTurn = deathColor == arena.GetArmyColor2();
3029 OpponentSprite * attackingHero = attackersTurn ? opponent1 : opponent2;
3030 OpponentSprite * defendingHero = attackersTurn ? opponent2 : opponent1;
3031 // 60% of joyful animation
3032 if ( attackingHero && Rand::Get( 1, 5 ) < 4 ) {
3033 attackingHero->SetAnimation( OP_JOY );
3034 }
3035 // 80% of sorrow animation otherwise
3036 else if ( defendingHero && Rand::Get( 1, 5 ) < 5 ) {
3037 defendingHero->SetAnimation( OP_SORROW );
3038 }
3039 }
3040
3041 // targets damage animation loop
3042 bool finishedAnimation = false;
3043 while ( le.HandleEvents() && !finishedAnimation ) {
3044 CheckGlobalEvents( le );
3045
3046 if ( Game::validateAnimationDelay( Game::BATTLE_FRAME_DELAY ) ) {
3047 bool redrawBattleField = false;
3048
3049 if ( attacker != nullptr ) {
3050 if ( attacker->isFinishAnimFrame() ) {
3051 attacker->SwitchAnimation( Monster_Info::STATIC );
3052 }
3053 else {
3054 attacker->IncreaseAnimFrame();
3055 }
3056
3057 redrawBattleField = true;
3058 }
3059 else {
3060 for ( TargetsInfo::iterator it = targets.begin(); it != targets.end(); ++it ) {
3061 if ( ( *it ).defender ) {
3062 redrawBattleField = true;
3063 break;
3064 }
3065 }
3066 }
3067
3068 if ( redrawBattleField ) {
3069 RedrawPartialStart();
3070 RedrawPartialFinish();
3071 }
3072
3073 const int finishedAnimationCount = std::count_if( targets.begin(), targets.end(), [&resistantTarget]( const TargetInfo & info ) {
3074 if ( info.defender == nullptr ) {
3075 return false;
3076 }
3077
3078 if ( resistantTarget.count( info.defender ) > 0 ) {
3079 return false;
3080 }
3081
3082 const int animationState = info.defender->GetAnimationState();
3083 if ( animationState != Monster_Info::WNCE && animationState != Monster_Info::KILL ) {
3084 return true;
3085 }
3086
3087 return TargetInfo::isFinishAnimFrame( info );
3088 } );
3089
3090 finishedAnimation = ( finish == finishedAnimationCount );
3091
3092 for ( TargetsInfo::iterator it = targets.begin(); it != targets.end(); ++it ) {
3093 if ( ( *it ).defender ) {
3094 if ( it->defender->isFinishAnimFrame() && it->defender->GetAnimationState() == Monster_Info::WNCE ) {
3095 it->defender->SwitchAnimation( Monster_Info::STATIC );
3096 }
3097 else {
3098 it->defender->IncreaseAnimFrame();
3099 }
3100 }
3101 }
3102 }
3103 }
3104
3105 // Fade away animation for destroyed mirror images
3106 if ( !mirrorImages.empty() )
3107 RedrawActionRemoveMirrorImage( mirrorImages );
3108 }
3109
RedrawActionMove(Unit & unit,const Indexes & path)3110 void Battle::Interface::RedrawActionMove( Unit & unit, const Indexes & path )
3111 {
3112 Indexes::const_iterator dst = path.begin();
3113 Bridge * bridge = Arena::GetBridge();
3114
3115 uint32_t frameDelay = Game::ApplyBattleSpeed( unit.animation.getMoveSpeed() );
3116 if ( unit.Modes( SP_HASTE ) ) {
3117 frameDelay = frameDelay * 65 / 100; // by 35% faster
3118 }
3119 else if ( unit.Modes( SP_SLOW ) ) {
3120 frameDelay = frameDelay * 150 / 100; // by 50% slower
3121 }
3122
3123 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
3124
3125 #ifdef DEBUG_LOG
3126 std::string msg = _( "Moved %{monster}: %{src}, %{dst}" );
3127 StringReplace( msg, "%{monster}", unit.GetName() );
3128 StringReplace( msg, "%{src}", unit.GetHeadIndex() );
3129 #else
3130 std::string msg = _( "Moved %{monster}" );
3131 StringReplace( msg, "%{monster}", unit.GetName() );
3132 #endif
3133
3134 _currentUnit = nullptr;
3135 _movingUnit = &unit;
3136
3137 while ( dst != path.end() ) {
3138 const Cell * cell = Board::GetCell( *dst );
3139 _movingPos = cell->GetPos().getPosition();
3140 bool show_anim = false;
3141
3142 if ( bridge && bridge->NeedDown( unit, *dst ) ) {
3143 _movingUnit = nullptr;
3144 unit.SwitchAnimation( Monster_Info::STATIC );
3145 bridge->Action( unit, *dst );
3146 _movingUnit = &unit;
3147 }
3148
3149 if ( unit.isWide() ) {
3150 if ( unit.GetTailIndex() == *dst )
3151 unit.SetReflection( !unit.isReflect() );
3152 else
3153 show_anim = true;
3154 }
3155 else {
3156 unit.UpdateDirection( cell->GetPos() );
3157 show_anim = true;
3158 }
3159
3160 if ( show_anim ) {
3161 AGG::PlaySound( unit.M82Move() );
3162 unit.SwitchAnimation( Monster_Info::MOVING );
3163 AnimateUnitWithDelay( unit, frameDelay );
3164 unit.SetPosition( *dst );
3165 }
3166
3167 // check for possible bridge close action, after unit's end of movement
3168 if ( bridge && bridge->AllowUp() ) {
3169 _movingUnit = nullptr;
3170 unit.SwitchAnimation( Monster_Info::STATIC );
3171 bridge->Action( unit, *dst );
3172 _movingUnit = &unit;
3173 }
3174
3175 ++dst;
3176 }
3177
3178 // restore
3179 _flyingUnit = nullptr;
3180 _movingUnit = nullptr;
3181 _currentUnit = nullptr;
3182 unit.SwitchAnimation( Monster_Info::STATIC );
3183
3184 #ifdef DEBUG_LOG
3185 StringReplace( msg, "%{dst}", unit.GetHeadIndex() );
3186 #endif
3187 status.SetMessage( msg, true );
3188 }
3189
RedrawActionFly(Unit & unit,const Position & pos)3190 void Battle::Interface::RedrawActionFly( Unit & unit, const Position & pos )
3191 {
3192 const int32_t destIndex = pos.GetHead()->GetIndex();
3193 const int32_t destTailIndex = unit.isWide() ? pos.GetTail()->GetIndex() : -1;
3194
3195 // check if we're already there
3196 if ( unit.GetPosition().contains( destIndex ) )
3197 return;
3198
3199 const fheroes2::Rect & pos1 = unit.GetRectPosition();
3200 const fheroes2::Rect & pos2 = Board::GetCell( destIndex )->GetPos();
3201
3202 fheroes2::Point destPos( pos1.x, pos1.y );
3203 fheroes2::Point targetPos( pos2.x, pos2.y );
3204
3205 if ( unit.isWide() && targetPos.x > destPos.x ) {
3206 targetPos.x -= CELLW; // this is needed to avoid extra cell shifting upon landing when we move to right side
3207 }
3208
3209 std::string msg = _( "Moved %{monster}: %{src}, %{dst}" );
3210 StringReplace( msg, "%{monster}", unit.GetName() );
3211 StringReplace( msg, "%{src}", unit.GetHeadIndex() );
3212
3213 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
3214
3215 const uint32_t step = unit.animation.getFlightSpeed();
3216 uint32_t frameDelay = Game::ApplyBattleSpeed( unit.animation.getMoveSpeed() );
3217 if ( unit.Modes( SP_HASTE ) ) {
3218 frameDelay = frameDelay * 8 / 10; // 20% faster
3219 }
3220 else if ( unit.Modes( SP_SLOW ) ) {
3221 frameDelay = frameDelay * 12 / 10; // 20% slower
3222 }
3223
3224 const std::vector<fheroes2::Point> points = GetEuclideanLine( destPos, targetPos, step );
3225 std::vector<fheroes2::Point>::const_iterator currentPoint = points.begin();
3226
3227 // cleanup
3228 _currentUnit = nullptr;
3229 _movingUnit = nullptr;
3230 _flyingUnit = nullptr;
3231
3232 Bridge * bridge = Arena::GetBridge();
3233
3234 // open the bridge if the unit should land on it
3235 if ( bridge ) {
3236 if ( bridge->NeedDown( unit, destIndex ) ) {
3237 bridge->Action( unit, destIndex );
3238 }
3239 else if ( unit.isWide() && bridge->NeedDown( unit, destTailIndex ) ) {
3240 bridge->Action( unit, destTailIndex );
3241 }
3242 }
3243
3244 // jump up
3245 _flyingUnit = nullptr;
3246 _movingUnit = &unit;
3247 _movingPos = currentPoint != points.end() ? *currentPoint : destPos;
3248 _flyingPos = destPos;
3249
3250 unit.SwitchAnimation( Monster_Info::FLY_UP );
3251 // Take off animation is 30% length on average (original value)
3252 AnimateUnitWithDelay( unit, frameDelay * 3 / 10 );
3253
3254 _movingUnit = nullptr;
3255 _flyingUnit = &unit;
3256 _flyingPos = _movingPos;
3257
3258 if ( currentPoint != points.end() )
3259 ++currentPoint;
3260
3261 unit.SwitchAnimation( Monster_Info::MOVING );
3262 while ( currentPoint != points.end() ) {
3263 _movingPos = *currentPoint;
3264
3265 AGG::PlaySound( unit.M82Move() );
3266 unit.animation.restartAnimation();
3267 AnimateUnitWithDelay( unit, frameDelay );
3268
3269 _flyingPos = _movingPos;
3270 ++currentPoint;
3271 }
3272
3273 unit.SetPosition( destIndex );
3274
3275 // landing
3276 _flyingUnit = nullptr;
3277 _movingUnit = &unit;
3278 _movingPos = targetPos;
3279
3280 std::vector<int> landAnim;
3281 landAnim.push_back( Monster_Info::FLY_LAND );
3282 landAnim.push_back( Monster_Info::STATIC );
3283 unit.SwitchAnimation( landAnim );
3284 AnimateUnitWithDelay( unit, frameDelay );
3285
3286 // restore
3287 _movingUnit = nullptr;
3288
3289 // check for possible bridge close action, after unit's end of movement
3290 if ( bridge && bridge->AllowUp() ) {
3291 bridge->Action( unit, destIndex );
3292 }
3293
3294 StringReplace( msg, "%{dst}", unit.GetHeadIndex() );
3295 status.SetMessage( msg, true );
3296 }
3297
RedrawActionResistSpell(const Unit & target,bool playSound)3298 void Battle::Interface::RedrawActionResistSpell( const Unit & target, bool playSound )
3299 {
3300 if ( playSound ) {
3301 AGG::PlaySound( M82::RSBRYFZL );
3302 }
3303 std::string str( _( "The %{name} resist the spell!" ) );
3304 StringReplace( str, "%{name}", target.GetName() );
3305 status.SetMessage( str, true );
3306 status.SetMessage( "", false );
3307 }
3308
RedrawActionSpellCastStatus(const Spell & spell,int32_t dst,const std::string & name,const TargetsInfo & targets)3309 void Battle::Interface::RedrawActionSpellCastStatus( const Spell & spell, int32_t dst, const std::string & name, const TargetsInfo & targets )
3310 {
3311 const Unit * target = !targets.empty() ? targets.front().defender : nullptr;
3312
3313 std::string msg;
3314
3315 if ( target && target->GetHeadIndex() == dst ) {
3316 msg = _( "%{name} casts %{spell} on the %{troop}." );
3317 StringReplace( msg, "%{troop}", target->GetName() );
3318 }
3319 else if ( spell.isApplyWithoutFocusObject() ) {
3320 msg = _( "%{name} casts %{spell}." );
3321 }
3322
3323 if ( !msg.empty() ) {
3324 StringReplace( msg, "%{name}", name );
3325 StringReplace( msg, "%{spell}", spell.GetName() );
3326
3327 status.SetMessage( msg, true );
3328 status.SetMessage( "", false );
3329 }
3330 }
3331
RedrawActionSpellCastPart1(const Spell & spell,s32 dst,const HeroBase * caster,const TargetsInfo & targets)3332 void Battle::Interface::RedrawActionSpellCastPart1( const Spell & spell, s32 dst, const HeroBase * caster, const TargetsInfo & targets )
3333 {
3334 Unit * target = !targets.empty() ? targets.front().defender : nullptr;
3335
3336 // set spell cast animation
3337 if ( caster ) {
3338 OpponentSprite * opponent = caster->GetColor() == arena.GetArmyColor1() ? opponent1 : opponent2;
3339 if ( opponent ) {
3340 opponent->SetAnimation( spell.isApplyWithoutFocusObject() ? OP_CAST_MASS : OP_CAST_UP );
3341 AnimateOpponents( opponent );
3342 }
3343 }
3344
3345 // without object
3346 switch ( spell.GetID() ) {
3347 case Spell::FIREBALL:
3348 RedrawTargetsWithFrameAnimation( dst, targets, ICN::FIREBALL, M82::FromSpell( spell.GetID() ) );
3349 break;
3350 case Spell::FIREBLAST:
3351 RedrawTargetsWithFrameAnimation( dst, targets, ICN::FIREBAL2, M82::FromSpell( spell.GetID() ) );
3352 break;
3353 case Spell::METEORSHOWER:
3354 RedrawTargetsWithFrameAnimation( dst, targets, ICN::METEOR, M82::FromSpell( spell.GetID() ), 1 );
3355 break;
3356 case Spell::COLDRING:
3357 RedrawActionColdRingSpell( dst, targets );
3358 break;
3359
3360 case Spell::MASSSHIELD:
3361 RedrawTargetsWithFrameAnimation( targets, ICN::SHIELD, M82::FromSpell( spell.GetID() ), false );
3362 break;
3363 case Spell::MASSCURE:
3364 RedrawTargetsWithFrameAnimation( targets, ICN::MAGIC01, M82::FromSpell( spell.GetID() ), false );
3365 break;
3366 case Spell::MASSHASTE:
3367 RedrawTargetsWithFrameAnimation( targets, ICN::HASTE, M82::FromSpell( spell.GetID() ), false );
3368 break;
3369 case Spell::MASSSLOW:
3370 RedrawTargetsWithFrameAnimation( targets, ICN::MAGIC02, M82::FromSpell( spell.GetID() ), false );
3371 break;
3372 case Spell::MASSBLESS:
3373 RedrawTargetsWithFrameAnimation( targets, ICN::BLESS, M82::FromSpell( spell.GetID() ), false );
3374 break;
3375 case Spell::MASSCURSE:
3376 RedrawTargetsWithFrameAnimation( targets, ICN::CURSE, M82::FromSpell( spell.GetID() ), false );
3377 break;
3378 case Spell::MASSDISPEL:
3379 RedrawTargetsWithFrameAnimation( targets, ICN::MAGIC07, M82::FromSpell( spell.GetID() ), false );
3380 break;
3381
3382 case Spell::DEATHRIPPLE:
3383 RedrawActionDeathWaveSpell( targets, 10 );
3384 break;
3385 case Spell::DEATHWAVE:
3386 RedrawActionDeathWaveSpell( targets, 15 );
3387 break;
3388
3389 case Spell::HOLYWORD:
3390 RedrawActionHolyShoutSpell( targets, 2 );
3391 break;
3392 case Spell::HOLYSHOUT:
3393 RedrawActionHolyShoutSpell( targets, 4 );
3394 break;
3395
3396 case Spell::ELEMENTALSTORM:
3397 RedrawActionElementalStormSpell( targets );
3398 break;
3399 case Spell::ARMAGEDDON:
3400 RedrawActionArmageddonSpell(); // hit everything
3401 break;
3402
3403 default:
3404 break;
3405 }
3406
3407 // with object
3408 if ( target ) {
3409 if ( spell.isResurrect() )
3410 RedrawActionResurrectSpell( *target, spell );
3411 else
3412 switch ( spell.GetID() ) {
3413 // simple spell animation
3414 case Spell::BLESS:
3415 RedrawTroopWithFrameAnimation( *target, ICN::BLESS, M82::FromSpell( spell.GetID() ), NONE );
3416 break;
3417 case Spell::BLIND:
3418 RedrawTroopWithFrameAnimation( *target, ICN::BLIND, M82::FromSpell( spell.GetID() ), NONE );
3419 break;
3420 case Spell::CURE:
3421 RedrawTroopWithFrameAnimation( *target, ICN::MAGIC01, M82::FromSpell( spell.GetID() ), NONE );
3422 break;
3423 case Spell::SLOW:
3424 RedrawTroopWithFrameAnimation( *target, ICN::MAGIC02, M82::FromSpell( spell.GetID() ), NONE );
3425 break;
3426 case Spell::SHIELD:
3427 RedrawTroopWithFrameAnimation( *target, ICN::SHIELD, M82::FromSpell( spell.GetID() ), NONE );
3428 break;
3429 case Spell::HASTE:
3430 RedrawTroopWithFrameAnimation( *target, ICN::HASTE, M82::FromSpell( spell.GetID() ), NONE );
3431 break;
3432 case Spell::CURSE:
3433 RedrawTroopWithFrameAnimation( *target, ICN::CURSE, M82::FromSpell( spell.GetID() ), NONE );
3434 break;
3435 case Spell::ANTIMAGIC:
3436 RedrawTroopWithFrameAnimation( *target, ICN::MAGIC06, M82::FromSpell( spell.GetID() ), NONE );
3437 break;
3438 case Spell::DISPEL:
3439 RedrawTroopWithFrameAnimation( *target, ICN::MAGIC07, M82::FromSpell( spell.GetID() ), NONE );
3440 break;
3441 case Spell::STONESKIN:
3442 RedrawTroopWithFrameAnimation( *target, ICN::STONSKIN, M82::FromSpell( spell.GetID() ), NONE );
3443 break;
3444 case Spell::STEELSKIN:
3445 RedrawTroopWithFrameAnimation( *target, ICN::STELSKIN, M82::FromSpell( spell.GetID() ), NONE );
3446 break;
3447 case Spell::PARALYZE:
3448 RedrawTroopWithFrameAnimation( *target, ICN::PARALYZE, M82::FromSpell( spell.GetID() ), NONE );
3449 break;
3450 case Spell::HYPNOTIZE:
3451 RedrawTroopWithFrameAnimation( *target, ICN::HYPNOTIZ, M82::FromSpell( spell.GetID() ), NONE );
3452 break;
3453 case Spell::DRAGONSLAYER:
3454 RedrawTroopWithFrameAnimation( *target, ICN::DRAGSLAY, M82::FromSpell( spell.GetID() ), NONE );
3455 break;
3456 case Spell::BERSERKER:
3457 RedrawTroopWithFrameAnimation( *target, ICN::BERZERK, M82::FromSpell( spell.GetID() ), NONE );
3458 break;
3459
3460 // uniq spell animation
3461 case Spell::LIGHTNINGBOLT:
3462 RedrawActionLightningBoltSpell( *target );
3463 break;
3464 case Spell::CHAINLIGHTNING:
3465 RedrawActionChainLightningSpell( targets );
3466 break;
3467 case Spell::ARROW:
3468 RedrawActionArrowSpell( *target );
3469 break;
3470 case Spell::COLDRAY:
3471 RedrawActionColdRaySpell( *target );
3472 break;
3473 case Spell::DISRUPTINGRAY:
3474 RedrawActionDisruptingRaySpell( *target );
3475 break;
3476 case Spell::BLOODLUST:
3477 RedrawActionBloodLustSpell( *target );
3478 break;
3479 case Spell::STONE:
3480 RedrawActionStoneSpell( *target );
3481 break;
3482 default:
3483 break;
3484 }
3485 }
3486
3487 if ( caster ) {
3488 OpponentSprite * opponent = caster->GetColor() == arena.GetArmyColor1() ? opponent1 : opponent2;
3489 if ( opponent ) {
3490 opponent->SetAnimation( ( target ) ? OP_CAST_UP_RETURN : OP_CAST_MASS_RETURN );
3491 AnimateOpponents( opponent );
3492 }
3493 }
3494 }
3495
RedrawActionSpellCastPart2(const Spell & spell,TargetsInfo & targets)3496 void Battle::Interface::RedrawActionSpellCastPart2( const Spell & spell, TargetsInfo & targets )
3497 {
3498 if ( spell.isDamage() ) {
3499 uint32_t killed = 0;
3500 uint32_t totalDamage = 0;
3501 uint32_t maximumDamage = 0;
3502 uint32_t damagedMonsters = 0;
3503
3504 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it ) {
3505 if ( !it->defender->isModes( CAP_MIRRORIMAGE ) ) {
3506 killed += ( *it ).killed;
3507
3508 ++damagedMonsters;
3509 totalDamage += it->damage;
3510 if ( maximumDamage < it->damage )
3511 maximumDamage = it->damage;
3512 }
3513 }
3514
3515 // targets damage animation
3516 RedrawActionWincesKills( targets );
3517
3518 if ( totalDamage > 0 ) {
3519 assert( damagedMonsters > 0 );
3520 std::string msg;
3521 if ( spell.isUndeadOnly() ) {
3522 if ( damagedMonsters == 1 ) {
3523 msg = _( "The %{spell} does %{damage} damage to one undead creature." );
3524 StringReplace( msg, "%{spell}", spell.GetName() );
3525 StringReplace( msg, "%{damage}", totalDamage );
3526 status.SetMessage( msg, true );
3527
3528 if ( killed > 0 ) {
3529 msg = _n( "1 creature perishes.", "%{count} creatures perish.", killed );
3530 StringReplace( msg, "%{count}", killed );
3531 status.SetMessage( msg, true );
3532 }
3533 }
3534 else {
3535 msg = _( "The %{spell} does %{damage} damage to all undead creatures." );
3536 StringReplace( msg, "%{spell}", spell.GetName() );
3537 StringReplace( msg, "%{damage}", maximumDamage );
3538 status.SetMessage( msg, true );
3539
3540 if ( killed > 0 ) {
3541 msg = _( "The %{spell} does %{damage} damage, %{count} creatures perish." );
3542 StringReplace( msg, "%{count}", killed );
3543 }
3544 else {
3545 msg = _( "The %{spell} does %{damage} damage." );
3546 }
3547
3548 StringReplace( msg, "%{spell}", spell.GetName() );
3549 StringReplace( msg, "%{damage}", totalDamage );
3550 status.SetMessage( msg, true );
3551 }
3552 }
3553 else if ( spell.isALiveOnly() ) {
3554 if ( damagedMonsters == 1 ) {
3555 msg = _( "The %{spell} does %{damage} damage to one living creature." );
3556 StringReplace( msg, "%{spell}", spell.GetName() );
3557 StringReplace( msg, "%{damage}", totalDamage );
3558 status.SetMessage( msg, true );
3559
3560 if ( killed > 0 ) {
3561 msg = _n( "1 creature perishes.", "%{count} creatures perish.", killed );
3562 StringReplace( msg, "%{count}", killed );
3563 status.SetMessage( msg, true );
3564 }
3565 }
3566 else {
3567 msg = _( "The %{spell} does %{damage} damage to all living creatures." );
3568 StringReplace( msg, "%{spell}", spell.GetName() );
3569 StringReplace( msg, "%{damage}", maximumDamage );
3570 status.SetMessage( msg, true );
3571
3572 if ( killed > 0 ) {
3573 msg = _( "The %{spell} does %{damage} damage, %{count} creatures perish." );
3574 StringReplace( msg, "%{count}", killed );
3575 }
3576 else {
3577 msg = _( "The %{spell} does %{damage} damage." );
3578 }
3579
3580 StringReplace( msg, "%{spell}", spell.GetName() );
3581 StringReplace( msg, "%{damage}", totalDamage );
3582 status.SetMessage( msg, true );
3583 }
3584 }
3585 else {
3586 msg = _( "The %{spell} does %{damage} damage." );
3587 StringReplace( msg, "%{spell}", spell.GetName() );
3588 StringReplace( msg, "%{damage}", totalDamage );
3589 status.SetMessage( msg, true );
3590
3591 if ( killed > 0 ) {
3592 msg = _n( "1 creature perishes.", "%{count} creatures perish.", killed );
3593 StringReplace( msg, "%{count}", killed );
3594 status.SetMessage( msg, true );
3595 }
3596 }
3597 }
3598 }
3599
3600 status.SetMessage( " ", false );
3601 _movingUnit = nullptr;
3602 }
3603
RedrawActionMonsterSpellCastStatus(const Unit & attacker,const TargetInfo & target)3604 void Battle::Interface::RedrawActionMonsterSpellCastStatus( const Unit & attacker, const TargetInfo & target )
3605 {
3606 const char * msg = nullptr;
3607
3608 switch ( attacker.GetID() ) {
3609 case Monster::UNICORN:
3610 msg = _( "The Unicorns' attack blinds the %{name}!" );
3611 break;
3612 case Monster::MEDUSA:
3613 msg = _( "The Medusas' gaze turns the %{name} to stone!" );
3614 break;
3615 case Monster::ROYAL_MUMMY:
3616 case Monster::MUMMY:
3617 msg = _( "The Mummies' curse falls upon the %{name}!" );
3618 break;
3619 case Monster::CYCLOPS:
3620 msg = _( "The %{name} are paralyzed by the Cyclopes!" );
3621 break;
3622 case Monster::ARCHMAGE:
3623 msg = _( "The Archmagi dispel all good spells on your %{name}!" );
3624 break;
3625 default:
3626 break;
3627 }
3628
3629 if ( msg ) {
3630 std::string str( msg );
3631 StringReplace( str, "%{name}", target.defender->GetName() );
3632
3633 status.SetMessage( str, true );
3634 status.SetMessage( "", false );
3635 }
3636 }
3637
RedrawActionLuck(const Unit & unit)3638 void Battle::Interface::RedrawActionLuck( const Unit & unit )
3639 {
3640 LocalEvent & le = LocalEvent::Get();
3641
3642 const bool isGoodLuck = unit.Modes( LUCK_GOOD );
3643 const fheroes2::Rect & pos = unit.GetRectPosition();
3644
3645 std::string msg = isGoodLuck ? _( "Good luck shines on the %{attacker}." ) : _( "Bad luck descends on the %{attacker}." );
3646 StringReplace( msg, "%{attacker}", unit.GetName() );
3647 status.SetMessage( msg, true );
3648
3649 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
3650 if ( isGoodLuck ) {
3651 const fheroes2::Sprite & luckSprite = fheroes2::AGG::GetICN( ICN::EXPMRL, 0 );
3652 const fheroes2::Sprite & unitSprite = fheroes2::AGG::GetICN( unit.GetMonsterSprite(), unit.GetFrame() );
3653
3654 int width = 2;
3655 fheroes2::Rect src( 0, 0, width, luckSprite.height() );
3656 src.x = ( luckSprite.width() - src.width ) / 2;
3657 int y = pos.y + pos.height - unitSprite.height() - src.height;
3658 if ( y < 0 )
3659 y = 0;
3660
3661 AGG::PlaySound( M82::GOODLUCK );
3662
3663 while ( le.HandleEvents() && Mixer::isPlaying( -1 ) ) {
3664 CheckGlobalEvents( le );
3665
3666 if ( width < luckSprite.width() && Game::validateAnimationDelay( Game::BATTLE_MISSILE_DELAY ) ) {
3667 RedrawPartialStart();
3668
3669 fheroes2::Blit( luckSprite, src.x, src.y, _mainSurface, pos.x + ( pos.width - src.width ) / 2, y, src.width, src.height );
3670
3671 RedrawPartialFinish();
3672
3673 src.width = width;
3674 src.x = ( luckSprite.width() - src.width ) / 2;
3675
3676 width += 3;
3677 }
3678 }
3679 }
3680 else {
3681 const int maxHeight = fheroes2::AGG::GetAbsoluteICNHeight( ICN::CLOUDLUK );
3682 int y = pos.y + pos.height + cellYOffset;
3683
3684 // move drawing position if it will clip outside of the battle window
3685 if ( y - maxHeight < 0 )
3686 y = maxHeight;
3687
3688 AGG::PlaySound( M82::BADLUCK );
3689
3690 int frameId = 0;
3691 while ( le.HandleEvents() && Mixer::isPlaying( -1 ) ) {
3692 CheckGlobalEvents( le );
3693
3694 if ( frameId < 8 && Game::validateAnimationDelay( Game::BATTLE_MISSILE_DELAY ) ) {
3695 RedrawPartialStart();
3696
3697 const fheroes2::Sprite & luckSprite = fheroes2::AGG::GetICN( ICN::CLOUDLUK, frameId );
3698 fheroes2::Blit( luckSprite, _mainSurface, pos.x + pos.width / 2 + luckSprite.x(), y + luckSprite.y() );
3699
3700 RedrawPartialFinish();
3701
3702 ++frameId;
3703 }
3704 }
3705 }
3706 }
3707
RedrawActionMorale(Unit & b,bool good)3708 void Battle::Interface::RedrawActionMorale( Unit & b, bool good )
3709 {
3710 std::string msg;
3711
3712 if ( good ) {
3713 msg = _( "High morale enables the %{monster} to attack again." );
3714 StringReplace( msg, "%{monster}", b.GetName() );
3715 status.SetMessage( msg, true );
3716 RedrawTroopWithFrameAnimation( b, ICN::MORALEG, M82::GOODMRLE, NONE );
3717 }
3718 else {
3719 msg = _( "Low morale causes the %{monster} to freeze in panic." );
3720 StringReplace( msg, "%{monster}", b.GetName() );
3721 status.SetMessage( msg, true );
3722 RedrawTroopWithFrameAnimation( b, ICN::MORALEB, M82::BADMRLE, WINCE );
3723 }
3724 }
3725
RedrawActionTowerPart1(const Tower & tower,const Unit & defender)3726 void Battle::Interface::RedrawActionTowerPart1( const Tower & tower, const Unit & defender )
3727 {
3728 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
3729 _currentUnit = nullptr;
3730
3731 const fheroes2::Point missileStart = tower.GetPortPosition();
3732 const fheroes2::Point targetPos = defender.GetCenterPoint();
3733 const double angle = GetAngle( missileStart, targetPos );
3734
3735 AGG::PlaySound( M82::KEEPSHOT );
3736
3737 // Keep missile == Orc missile
3738 RedrawMissileAnimation( missileStart, targetPos, angle, Monster::ORC );
3739 }
3740
RedrawActionTowerPart2(const Tower & tower,const TargetInfo & target)3741 void Battle::Interface::RedrawActionTowerPart2( const Tower & tower, const TargetInfo & target )
3742 {
3743 TargetsInfo targets;
3744 targets.push_back( target );
3745 const bool isMirror = target.defender->isModes( CAP_MIRRORIMAGE );
3746
3747 // targets damage animation
3748 RedrawActionWincesKills( targets );
3749
3750 // draw status for first defender
3751 std::string msg = _( "%{tower} does %{damage} damage." );
3752 StringReplace( msg, "%{tower}", tower.GetName() );
3753 StringReplace( msg, "%{damage}", target.damage );
3754 if ( target.killed ) {
3755 msg += ' ';
3756 msg.append( _n( "1 %{defender} perishes.", "%{count} %{defender} perish.", target.killed ) );
3757 StringReplace( msg, "%{count}", target.killed );
3758 StringReplace( msg, "%{defender}", target.defender->GetPluralName( target.killed ) );
3759 }
3760
3761 if ( !isMirror ) {
3762 status.SetMessage( msg, true );
3763 status.SetMessage( "", false );
3764 }
3765
3766 _movingUnit = nullptr;
3767 }
3768
RedrawActionCatapult(int target,bool hit)3769 void Battle::Interface::RedrawActionCatapult( int target, bool hit )
3770 {
3771 LocalEvent & le = LocalEvent::Get();
3772
3773 const fheroes2::Sprite & missile = fheroes2::AGG::GetICN( ICN::BOULDER, 0 );
3774 const fheroes2::Rect & area = GetArea();
3775
3776 AGG::PlaySound( M82::CATSND00 );
3777
3778 // catapult animation
3779 while ( le.HandleEvents( false ) && catapult_frame < 6 ) {
3780 CheckGlobalEvents( le );
3781
3782 if ( Game::validateAnimationDelay( Game::BATTLE_CATAPULT_DELAY ) ) {
3783 Redraw();
3784 ++catapult_frame;
3785 }
3786 }
3787
3788 // boulder animation
3789 fheroes2::Point pt1( 90, 220 );
3790 fheroes2::Point pt2 = Catapult::GetTargetPosition( target, hit );
3791 fheroes2::Point max( 300, 20 );
3792
3793 pt1.x += area.x;
3794 pt2.x += area.x;
3795 max.x += area.x;
3796 pt1.y += area.y;
3797 pt2.y += area.y;
3798 max.y += area.y;
3799
3800 const std::vector<fheroes2::Point> points = GetArcPoints( pt1, pt2, max, missile.width() );
3801 std::vector<fheroes2::Point>::const_iterator pnt = points.begin();
3802
3803 while ( le.HandleEvents( false ) && pnt != points.end() ) {
3804 CheckGlobalEvents( le );
3805
3806 if ( Game::validateAnimationDelay( Game::BATTLE_CATAPULT_BOULDER_DELAY ) ) {
3807 if ( catapult_frame < 9 )
3808 ++catapult_frame;
3809
3810 RedrawPartialStart();
3811 fheroes2::Blit( missile, _mainSurface, pnt->x, pnt->y );
3812 RedrawPartialFinish();
3813 ++pnt;
3814 }
3815 }
3816
3817 // draw cloud
3818 const int icn = hit ? ICN::LICHCLOD : ICN::SMALCLOD;
3819 uint32_t frame = 0;
3820
3821 AGG::PlaySound( M82::CATSND02 );
3822
3823 while ( le.HandleEvents() && frame < fheroes2::AGG::GetICNCount( icn ) ) {
3824 CheckGlobalEvents( le );
3825
3826 if ( Game::validateAnimationDelay( Game::BATTLE_CATAPULT_CLOUD_DELAY ) ) {
3827 if ( catapult_frame < 9 )
3828 ++catapult_frame;
3829
3830 RedrawPartialStart();
3831 const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( icn, frame );
3832 fheroes2::Blit( sprite, _mainSurface, pt2.x + sprite.x(), pt2.y + sprite.y() );
3833 RedrawPartialFinish();
3834
3835 ++frame;
3836 }
3837 }
3838
3839 catapult_frame = 0;
3840 }
3841
RedrawActionArrowSpell(const Unit & target)3842 void Battle::Interface::RedrawActionArrowSpell( const Unit & target )
3843 {
3844 const HeroBase * caster = arena.GetCurrentCommander();
3845
3846 if ( caster ) {
3847 const fheroes2::Point missileStart = caster == opponent1->GetHero() ? opponent1->GetCastPosition() : opponent2->GetCastPosition();
3848
3849 const fheroes2::Point targetPos = target.GetCenterPoint();
3850 const double angle = GetAngle( missileStart, targetPos );
3851
3852 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
3853 AGG::PlaySound( M82::MAGCAROW );
3854
3855 // Magic arrow == Archer missile
3856 RedrawMissileAnimation( missileStart, targetPos, angle, Monster::ARCHER );
3857 }
3858 }
3859
RedrawActionTeleportSpell(Unit & target,s32 dst)3860 void Battle::Interface::RedrawActionTeleportSpell( Unit & target, s32 dst )
3861 {
3862 LocalEvent & le = LocalEvent::Get();
3863
3864 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
3865
3866 uint32_t currentAlpha = target.GetCustomAlpha();
3867
3868 AGG::PlaySound( M82::TELPTOUT );
3869
3870 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
3871
3872 while ( le.HandleEvents() && Mixer::isPlaying( -1 ) ) {
3873 CheckGlobalEvents( le );
3874
3875 if ( currentAlpha > 0 && Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
3876 currentAlpha -= 15;
3877 target.SetCustomAlpha( currentAlpha );
3878 Redraw();
3879 }
3880 }
3881
3882 currentAlpha = 0;
3883 Redraw();
3884
3885 target.SetPosition( dst );
3886 AGG::PlaySound( M82::TELPTIN );
3887
3888 while ( le.HandleEvents() && Mixer::isPlaying( -1 ) ) {
3889 CheckGlobalEvents( le );
3890
3891 if ( currentAlpha <= 240 && Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
3892 currentAlpha += 15;
3893 target.SetCustomAlpha( currentAlpha );
3894 Redraw();
3895 }
3896 }
3897
3898 target.SetCustomAlpha( 255 );
3899 }
3900
RedrawActionSummonElementalSpell(Unit & target)3901 void Battle::Interface::RedrawActionSummonElementalSpell( Unit & target )
3902 {
3903 LocalEvent & le = LocalEvent::Get();
3904
3905 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
3906
3907 uint32_t currentAlpha = 0;
3908
3909 AGG::PlaySound( M82::SUMNELM );
3910
3911 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
3912
3913 while ( le.HandleEvents() && currentAlpha < 220 ) {
3914 CheckGlobalEvents( le );
3915
3916 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
3917 currentAlpha += 20;
3918 target.SetCustomAlpha( currentAlpha );
3919 Redraw();
3920 }
3921 }
3922
3923 target.SetCustomAlpha( 255 );
3924 }
3925
RedrawActionMirrorImageSpell(const Unit & target,const Position & pos)3926 void Battle::Interface::RedrawActionMirrorImageSpell( const Unit & target, const Position & pos )
3927 {
3928 LocalEvent & le = LocalEvent::Get();
3929
3930 fheroes2::Sprite sprite = fheroes2::AGG::GetICN( target.GetMonsterSprite(), target.GetFrame() );
3931 fheroes2::ApplyPalette( sprite, PAL::GetPalette( PAL::PaletteType::MIRROR_IMAGE ) );
3932
3933 const fheroes2::Rect & rt1 = target.GetRectPosition();
3934 const fheroes2::Rect & rt2 = pos.GetRect();
3935
3936 const std::vector<fheroes2::Point> points = GetLinePoints( rt1.getPosition(), rt2.getPosition(), 5 );
3937 std::vector<fheroes2::Point>::const_iterator pnt = points.begin();
3938
3939 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
3940 AGG::PlaySound( M82::MIRRORIM );
3941
3942 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
3943
3944 while ( le.HandleEvents() && pnt != points.end() ) {
3945 CheckGlobalEvents( le );
3946
3947 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
3948 const fheroes2::Point & sp = GetTroopPosition( target, sprite );
3949
3950 RedrawPartialStart();
3951 fheroes2::Blit( sprite, _mainSurface, sp.x - rt1.x + ( *pnt ).x, sp.y - rt1.y + ( *pnt ).y, target.isReflect() );
3952 RedrawPartialFinish();
3953
3954 ++pnt;
3955 }
3956 }
3957
3958 status.SetMessage( _( "The mirror image is created." ), true );
3959 }
3960
RedrawLightningOnTargets(const std::vector<fheroes2::Point> & points,const fheroes2::Rect & drawRoi)3961 void Battle::Interface::RedrawLightningOnTargets( const std::vector<fheroes2::Point> & points, const fheroes2::Rect & drawRoi )
3962 {
3963 if ( points.size() < 2 )
3964 return;
3965
3966 const fheroes2::Point roiOffset( drawRoi.x, drawRoi.y );
3967
3968 LocalEvent & le = LocalEvent::Get();
3969 Cursor & cursor = Cursor::Get();
3970 cursor.SetThemes( Cursor::WAR_POINTER );
3971
3972 AGG::PlaySound( points.size() > 2 ? M82::CHAINLTE : M82::LIGHTBLT );
3973
3974 for ( size_t i = 1; i < points.size(); ++i ) {
3975 const fheroes2::Point & startingPos = points[i - 1];
3976 const fheroes2::Point & endPos = points[i];
3977
3978 const std::vector<std::pair<LightningPoint, LightningPoint> > & lightningBolt = GenerateLightning( startingPos + roiOffset, endPos + roiOffset );
3979 fheroes2::Rect roi;
3980 const bool isHorizontalBolt = std::abs( startingPos.x - endPos.x ) > std::abs( startingPos.y - endPos.y );
3981 const bool isForwardDirection = isHorizontalBolt ? ( endPos.x > startingPos.x ) : ( endPos.y > startingPos.y );
3982 const int animationStep = 100;
3983
3984 if ( isHorizontalBolt ) {
3985 roi.height = drawRoi.height;
3986 if ( isForwardDirection ) {
3987 roi.x = 0;
3988 roi.width = startingPos.x;
3989 }
3990 else {
3991 roi.x = startingPos.x;
3992 roi.width = drawRoi.width - startingPos.x;
3993 }
3994 }
3995 else {
3996 roi.width = drawRoi.width;
3997 if ( isForwardDirection ) {
3998 roi.y = 0;
3999 roi.height = startingPos.y;
4000 }
4001 else {
4002 roi.y = startingPos.y;
4003 roi.height = drawRoi.height - startingPos.y;
4004 }
4005 }
4006
4007 while ( le.HandleEvents() && ( ( isHorizontalBolt && roi.width < drawRoi.width ) || ( !isHorizontalBolt && roi.height < drawRoi.height ) ) ) {
4008 if ( Game::validateAnimationDelay( Game::BATTLE_DISRUPTING_DELAY ) ) {
4009 if ( isHorizontalBolt ) {
4010 if ( isForwardDirection ) {
4011 roi.width += animationStep;
4012 }
4013 else {
4014 roi.width += animationStep;
4015 roi.x -= animationStep;
4016 }
4017
4018 if ( roi.x < 0 )
4019 roi.x = 0;
4020 if ( roi.width > drawRoi.width )
4021 roi.width = drawRoi.width;
4022 }
4023 else {
4024 if ( isForwardDirection ) {
4025 roi.height += animationStep;
4026 }
4027 else {
4028 roi.height += animationStep;
4029 roi.y -= animationStep;
4030 }
4031
4032 if ( roi.y < 0 )
4033 roi.y = 0;
4034 if ( roi.height > drawRoi.height )
4035 roi.height = drawRoi.height;
4036 }
4037
4038 RedrawPartialStart();
4039
4040 RedrawLightning( lightningBolt, fheroes2::GetColorId( 0xff, 0xff, 0 ), _mainSurface,
4041 fheroes2::Rect( roi.x + roiOffset.x, roi.y + roiOffset.y, roi.width, roi.height ) );
4042 fheroes2::ApplyPalette( _mainSurface, 7 );
4043
4044 RedrawPartialFinish();
4045 }
4046 }
4047 }
4048
4049 // small delay to display fully drawn lightning
4050 fheroes2::delayforMs( 100 );
4051
4052 uint32_t frame = 0;
4053 while ( le.HandleEvents() && frame < fheroes2::AGG::GetICNCount( ICN::SPARKS ) ) {
4054 CheckGlobalEvents( le );
4055
4056 if ( ( frame == 0 ) || Game::validateAnimationDelay( Game::BATTLE_DISRUPTING_DELAY ) ) {
4057 RedrawPartialStart();
4058
4059 const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::SPARKS, frame );
4060
4061 for ( size_t i = 1; i < points.size(); ++i ) {
4062 fheroes2::Point pt = points[i] - fheroes2::Point( sprite.width() / 2, 0 ) + roiOffset;
4063 fheroes2::Blit( sprite, _mainSurface, pt.x, pt.y );
4064 }
4065 RedrawPartialFinish();
4066
4067 ++frame;
4068 }
4069 }
4070 }
4071
RedrawActionLightningBoltSpell(const Unit & target)4072 void Battle::Interface::RedrawActionLightningBoltSpell( const Unit & target )
4073 {
4074 _currentUnit = nullptr;
4075
4076 const fheroes2::Point startingPos = arena.GetCurrentCommander() == opponent1->GetHero() ? opponent1->GetCastPosition() : opponent2->GetCastPosition();
4077 const fheroes2::Rect & pos = target.GetRectPosition();
4078 const fheroes2::Point endPos( pos.x + pos.width / 2, pos.y );
4079
4080 std::vector<fheroes2::Point> points;
4081 points.push_back( startingPos );
4082 points.push_back( endPos );
4083
4084 RedrawLightningOnTargets( points, _surfaceInnerArea );
4085 }
4086
RedrawActionChainLightningSpell(const TargetsInfo & targets)4087 void Battle::Interface::RedrawActionChainLightningSpell( const TargetsInfo & targets )
4088 {
4089 const fheroes2::Point startingPos = arena.GetCurrentCommander() == opponent1->GetHero() ? opponent1->GetCastPosition() : opponent2->GetCastPosition();
4090 std::vector<fheroes2::Point> points;
4091 points.push_back( startingPos );
4092
4093 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it ) {
4094 const fheroes2::Rect & pos = it->defender->GetRectPosition();
4095 points.push_back( fheroes2::Point( pos.x + pos.width / 2, pos.y ) );
4096 }
4097
4098 RedrawLightningOnTargets( points, _surfaceInnerArea );
4099 }
4100
RedrawActionBloodLustSpell(const Unit & target)4101 void Battle::Interface::RedrawActionBloodLustSpell( const Unit & target )
4102 {
4103 LocalEvent & le = LocalEvent::Get();
4104
4105 fheroes2::Sprite unitSprite = fheroes2::AGG::GetICN( target.GetMonsterSprite(), target.GetFrame() );
4106
4107 std::vector<std::vector<uint8_t> > originalPalette;
4108 if ( target.Modes( SP_STONE ) ) {
4109 originalPalette.push_back( PAL::GetPalette( PAL::PaletteType::GRAY ) );
4110 }
4111 else if ( target.Modes( CAP_MIRRORIMAGE ) ) {
4112 originalPalette.push_back( PAL::GetPalette( PAL::PaletteType::MIRROR_IMAGE ) );
4113 }
4114
4115 if ( !originalPalette.empty() ) {
4116 for ( size_t i = 1; i < originalPalette.size(); ++i ) {
4117 originalPalette[0] = PAL::CombinePalettes( originalPalette[0], originalPalette[i] );
4118 }
4119 fheroes2::ApplyPalette( unitSprite, originalPalette[0] );
4120 }
4121
4122 std::vector<uint8_t> convert = PAL::GetPalette( PAL::PaletteType::RED );
4123 if ( !originalPalette.empty() ) {
4124 convert = PAL::CombinePalettes( PAL::GetPalette( PAL::PaletteType::GRAY ), convert );
4125 }
4126
4127 fheroes2::Sprite bloodlustEffect( unitSprite );
4128 fheroes2::ApplyPalette( bloodlustEffect, convert );
4129
4130 fheroes2::Sprite mixSprite( unitSprite );
4131
4132 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
4133
4134 _currentUnit = ⌖
4135 b_current_sprite = &mixSprite;
4136
4137 const uint32_t bloodlustDelay = 1800 / 20;
4138 // duration is 1900ms
4139 AGG::PlaySound( M82::BLOODLUS );
4140
4141 uint32_t alpha = 0;
4142 uint32_t frame = 0;
4143 while ( le.HandleEvents() && Mixer::isPlaying( -1 ) ) {
4144 CheckGlobalEvents( le );
4145
4146 if ( frame < 20 && Game::validateCustomAnimationDelay( bloodlustDelay ) ) {
4147 mixSprite = unitSprite;
4148 fheroes2::AlphaBlit( bloodlustEffect, mixSprite, static_cast<uint8_t>( alpha ) );
4149 Redraw();
4150
4151 alpha += ( frame < 10 ) ? 20 : -20;
4152 ++frame;
4153 }
4154 }
4155
4156 _currentUnit = nullptr;
4157 b_current_sprite = nullptr;
4158 }
4159
RedrawActionStoneSpell(const Unit & target)4160 void Battle::Interface::RedrawActionStoneSpell( const Unit & target )
4161 {
4162 LocalEvent & le = LocalEvent::Get();
4163
4164 const fheroes2::Sprite & unitSprite = fheroes2::AGG::GetICN( target.GetMonsterSprite(), target.GetFrame() );
4165
4166 fheroes2::Sprite stoneEffect( unitSprite );
4167 fheroes2::ApplyPalette( stoneEffect, PAL::GetPalette( PAL::PaletteType::GRAY ) );
4168
4169 fheroes2::Sprite mixSprite( unitSprite );
4170
4171 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
4172
4173 _currentUnit = ⌖
4174 b_current_sprite = &mixSprite;
4175
4176 AGG::PlaySound( M82::PARALIZE );
4177
4178 uint32_t alpha = 0;
4179 uint32_t frame = 0;
4180 while ( le.HandleEvents() && Mixer::isPlaying( -1 ) ) {
4181 CheckGlobalEvents( le );
4182
4183 if ( frame < 25 && Game::validateCustomAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4184 mixSprite = fheroes2::Sprite( unitSprite );
4185 fheroes2::AlphaBlit( stoneEffect, mixSprite, static_cast<uint8_t>( alpha ) );
4186 Redraw();
4187
4188 alpha += 10;
4189 ++frame;
4190 }
4191 }
4192
4193 _currentUnit = nullptr;
4194 b_current_sprite = nullptr;
4195 }
4196
RedrawActionResurrectSpell(Unit & target,const Spell & spell)4197 void Battle::Interface::RedrawActionResurrectSpell( Unit & target, const Spell & spell )
4198 {
4199 LocalEvent & le = LocalEvent::Get();
4200
4201 if ( !target.isValid() ) {
4202 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
4203
4204 while ( le.HandleEvents() && !target.isFinishAnimFrame() ) {
4205 CheckGlobalEvents( le );
4206
4207 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4208 Redraw();
4209 target.IncreaseAnimFrame();
4210 }
4211 }
4212 }
4213
4214 AGG::PlaySound( M82::FromSpell( spell.GetID() ) );
4215
4216 RedrawTroopWithFrameAnimation( target, ICN::YINYANG, M82::UNKNOWN, target.GetHitPoints() == 0 ? RESURRECT : NONE );
4217 }
4218
RedrawActionColdRaySpell(Unit & target)4219 void Battle::Interface::RedrawActionColdRaySpell( Unit & target )
4220 {
4221 RedrawRaySpell( target, ICN::COLDRAY, M82::COLDRAY, 18 );
4222 RedrawTroopWithFrameAnimation( target, ICN::ICECLOUD, M82::UNKNOWN, NONE );
4223 }
4224
RedrawRaySpell(const Unit & target,int spellICN,int spellSound,int32_t size)4225 void Battle::Interface::RedrawRaySpell( const Unit & target, int spellICN, int spellSound, int32_t size )
4226 {
4227 Cursor & cursor = Cursor::Get();
4228 LocalEvent & le = LocalEvent::Get();
4229
4230 // Casting hero position
4231 const fheroes2::Point startingPos = arena.GetCurrentCommander() == opponent1->GetHero() ? opponent1->GetCastPosition() : opponent2->GetCastPosition();
4232 const fheroes2::Point targetPos = target.GetCenterPoint();
4233
4234 const std::vector<fheroes2::Point> path = GetEuclideanLine( startingPos, targetPos, size );
4235 const uint32_t spriteCount = fheroes2::AGG::GetICNCount( spellICN );
4236
4237 cursor.SetThemes( Cursor::WAR_POINTER );
4238 AGG::PlaySound( spellSound );
4239
4240 size_t i = 0;
4241 while ( le.HandleEvents() && i < path.size() ) {
4242 CheckGlobalEvents( le );
4243
4244 if ( Game::validateAnimationDelay( Game::BATTLE_DISRUPTING_DELAY ) ) {
4245 const uint32_t frame = static_cast<uint32_t>( i * spriteCount / path.size() ); // it's safe to do such as i <= path.size()
4246 const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( spellICN, frame );
4247 fheroes2::Blit( sprite, _mainSurface, path[i].x - sprite.width() / 2, path[i].y - sprite.height() / 2 );
4248 RedrawPartialFinish();
4249 ++i;
4250 }
4251 }
4252 }
4253
RedrawActionDisruptingRaySpell(const Unit & target)4254 void Battle::Interface::RedrawActionDisruptingRaySpell( const Unit & target )
4255 {
4256 LocalEvent & le = LocalEvent::Get();
4257
4258 RedrawRaySpell( target, ICN::DISRRAY, M82::DISRUPTR, 24 );
4259
4260 // Part 2 - ripple effect
4261 const fheroes2::Sprite & unitSprite = fheroes2::AGG::GetICN( target.GetMonsterSprite(), target.GetFrame() );
4262 fheroes2::Sprite rippleSprite;
4263
4264 const Unit * old_current = _currentUnit;
4265 _currentUnit = ⌖
4266 _movingPos = fheroes2::Point( 0, 0 );
4267
4268 uint32_t frame = 0;
4269 while ( le.HandleEvents() && frame < 60 ) {
4270 CheckGlobalEvents( le );
4271
4272 if ( Game::validateAnimationDelay( Game::BATTLE_DISRUPTING_DELAY ) ) {
4273 rippleSprite = fheroes2::CreateRippleEffect( unitSprite, frame );
4274 rippleSprite.setPosition( unitSprite.x(), unitSprite.y() );
4275
4276 b_current_sprite = &rippleSprite;
4277 Redraw();
4278
4279 frame += 2;
4280 }
4281 }
4282
4283 _currentUnit = old_current;
4284 b_current_sprite = nullptr;
4285 }
4286
RedrawActionDeathWaveSpell(const TargetsInfo & targets,int strength)4287 void Battle::Interface::RedrawActionDeathWaveSpell( const TargetsInfo & targets, int strength )
4288 {
4289 Cursor & cursor = Cursor::Get();
4290 LocalEvent & le = LocalEvent::Get();
4291 _currentUnit = nullptr;
4292 cursor.SetThemes( Cursor::WAR_POINTER );
4293
4294 fheroes2::Rect area = GetArea();
4295 area.height -= 36;
4296
4297 const fheroes2::Sprite & copy = fheroes2::Crop( _mainSurface, area.x, area.y, area.width, area.height );
4298 const int waveLength = strength * 2 + 10;
4299
4300 AGG::PlaySound( M82::MNRDEATH );
4301
4302 int position = 10;
4303 while ( le.HandleEvents() && position < area.width + waveLength ) {
4304 CheckGlobalEvents( le );
4305
4306 if ( Game::validateAnimationDelay( Game::BATTLE_DISRUPTING_DELAY ) ) {
4307 fheroes2::Blit( fheroes2::CreateDeathWaveEffect( copy, position, waveLength, strength ), _mainSurface );
4308 RedrawPartialFinish();
4309
4310 position += 3;
4311 }
4312 }
4313
4314 RedrawTargetsWithFrameAnimation( targets, ICN::REDDEATH, M82::UNKNOWN, true );
4315 }
4316
RedrawActionColdRingSpell(s32 dst,const TargetsInfo & targets)4317 void Battle::Interface::RedrawActionColdRingSpell( s32 dst, const TargetsInfo & targets )
4318 {
4319 LocalEvent & le = LocalEvent::Get();
4320
4321 const int icn = ICN::COLDRING;
4322 const int m82 = M82::FromSpell( Spell::COLDRING );
4323 uint32_t frame = 0;
4324 const fheroes2::Rect & center = Board::GetCell( dst )->GetPos();
4325
4326 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
4327
4328 // set WNCE
4329 _currentUnit = nullptr;
4330 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4331 if ( ( *it ).defender && ( *it ).damage )
4332 ( *it ).defender->SwitchAnimation( Monster_Info::WNCE );
4333
4334 if ( M82::UNKNOWN != m82 )
4335 AGG::PlaySound( m82 );
4336
4337 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
4338
4339 while ( le.HandleEvents() && frame < fheroes2::AGG::GetICNCount( icn ) ) {
4340 CheckGlobalEvents( le );
4341
4342 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4343 RedrawPartialStart();
4344
4345 const fheroes2::Sprite & sprite1 = fheroes2::AGG::GetICN( icn, frame );
4346 fheroes2::Blit( sprite1, _mainSurface, center.x + center.width / 2 + sprite1.x(), center.y + center.height / 2 + sprite1.y() );
4347 const fheroes2::Sprite & sprite2 = fheroes2::AGG::GetICN( icn, frame );
4348 fheroes2::Blit( sprite2, _mainSurface, center.x + center.width / 2 - sprite2.width() - sprite2.x(), center.y + center.height / 2 + sprite2.y(), true );
4349 RedrawPartialFinish();
4350
4351 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4352 if ( ( *it ).defender && ( *it ).damage )
4353 ( *it ).defender->IncreaseAnimFrame( false );
4354 ++frame;
4355 }
4356 }
4357
4358 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4359 if ( ( *it ).defender ) {
4360 ( *it ).defender->SwitchAnimation( Monster_Info::STATIC );
4361 _currentUnit = nullptr;
4362 }
4363 }
4364
RedrawActionHolyShoutSpell(const TargetsInfo & targets,int strength)4365 void Battle::Interface::RedrawActionHolyShoutSpell( const TargetsInfo & targets, int strength )
4366 {
4367 Cursor & cursor = Cursor::Get();
4368 LocalEvent & le = LocalEvent::Get();
4369
4370 cursor.SetThemes( Cursor::WAR_POINTER );
4371
4372 const fheroes2::Image original( _mainSurface );
4373 const fheroes2::Image blurred = fheroes2::CreateBlurredImage( _mainSurface, strength );
4374
4375 _currentUnit = nullptr;
4376 AGG::PlaySound( M82::MASSCURS );
4377
4378 const uint32_t spellcastDelay = Game::ApplyBattleSpeed( 3000 ) / 20;
4379 uint32_t frame = 0;
4380 uint8_t alpha = 30;
4381
4382 while ( le.HandleEvents() && frame < 20 ) {
4383 CheckGlobalEvents( le );
4384
4385 if ( Game::validateCustomAnimationDelay( spellcastDelay ) ) {
4386 // stay at maximum blur for 2 frames
4387 if ( frame < 9 || frame > 10 ) {
4388 fheroes2::Copy( original, _mainSurface );
4389 fheroes2::AlphaBlit( blurred, _mainSurface, alpha );
4390 RedrawPartialFinish();
4391
4392 alpha += ( frame < 10 ) ? 25 : -25;
4393 }
4394 ++frame;
4395 }
4396 }
4397
4398 RedrawTargetsWithFrameAnimation( targets, ICN::MAGIC08, M82::UNKNOWN, true );
4399 }
4400
RedrawActionElementalStormSpell(const TargetsInfo & targets)4401 void Battle::Interface::RedrawActionElementalStormSpell( const TargetsInfo & targets )
4402 {
4403 LocalEvent & le = LocalEvent::Get();
4404
4405 const int icn = ICN::STORM;
4406 const int m82 = M82::FromSpell( Spell::ELEMENTALSTORM );
4407 const int spriteSize = 54;
4408 const uint32_t icnCount = fheroes2::AGG::GetICNCount( icn );
4409
4410 std::vector<fheroes2::Sprite> spriteCache;
4411 spriteCache.reserve( icnCount );
4412 for ( uint32_t i = 0; i < icnCount; ++i ) {
4413 spriteCache.push_back( fheroes2::AGG::GetICN( icn, i ) );
4414 }
4415
4416 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
4417
4418 _currentUnit = nullptr;
4419 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4420 if ( ( *it ).defender && ( *it ).damage )
4421 ( *it ).defender->SwitchAnimation( Monster_Info::WNCE );
4422
4423 if ( M82::UNKNOWN != m82 )
4424 AGG::PlaySound( m82 );
4425
4426 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
4427
4428 uint32_t frame = 0;
4429 while ( le.HandleEvents() && frame < 60 ) {
4430 CheckGlobalEvents( le );
4431
4432 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4433 RedrawPartialStart();
4434
4435 if ( icnCount > 0 ) {
4436 for ( int x = 0; x * spriteSize < _surfaceInnerArea.width; ++x ) {
4437 const int idX = frame + x * 3;
4438 const int offsetX = x * spriteSize;
4439 for ( int y = 0; y * spriteSize < _surfaceInnerArea.height; ++y ) {
4440 const fheroes2::Sprite & sprite = spriteCache[( idX + y ) % icnCount];
4441 fheroes2::Blit( sprite, _mainSurface, offsetX + sprite.x(), y * spriteSize + sprite.y() );
4442 }
4443 }
4444 }
4445
4446 RedrawPartialFinish();
4447
4448 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4449 if ( ( *it ).defender && ( *it ).damage )
4450 ( *it ).defender->IncreaseAnimFrame( false );
4451 ++frame;
4452 }
4453 }
4454
4455 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4456 if ( ( *it ).defender ) {
4457 ( *it ).defender->SwitchAnimation( Monster_Info::STATIC );
4458 _currentUnit = nullptr;
4459 }
4460 }
4461
RedrawActionArmageddonSpell()4462 void Battle::Interface::RedrawActionArmageddonSpell()
4463 {
4464 Cursor & cursor = Cursor::Get();
4465 LocalEvent & le = LocalEvent::Get();
4466 fheroes2::Rect area = GetArea();
4467
4468 area.height -= 37;
4469
4470 fheroes2::Image spriteWhitening( area.width, area.height );
4471 fheroes2::Image spriteReddish( area.width, area.height );
4472 fheroes2::Copy( _mainSurface, area.x, area.y, spriteWhitening, 0, 0, area.width, area.height );
4473 fheroes2::Copy( _mainSurface, area.x, area.y, spriteReddish, 0, 0, area.width, area.height );
4474
4475 cursor.SetThemes( Cursor::WAR_POINTER );
4476
4477 _currentUnit = nullptr;
4478 AGG::PlaySound( M82::ARMGEDN );
4479 u32 alpha = 10;
4480
4481 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
4482
4483 while ( le.HandleEvents() && alpha < 100 ) {
4484 CheckGlobalEvents( le );
4485
4486 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4487 fheroes2::ApplyPalette( spriteWhitening, 9 );
4488 fheroes2::Blit( spriteWhitening, _mainSurface, area.x, area.y );
4489 RedrawPartialFinish();
4490
4491 alpha += 10;
4492 }
4493 }
4494
4495 fheroes2::ApplyPalette( spriteReddish, PAL::GetPalette( PAL::PaletteType::RED ) );
4496 fheroes2::Copy( spriteReddish, 0, 0, _mainSurface, area.x, area.y, area.width, area.height );
4497
4498 while ( le.HandleEvents() && Mixer::isPlaying( -1 ) ) {
4499 CheckGlobalEvents( le );
4500
4501 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4502 const int32_t offsetX = static_cast<int32_t>( Rand::Get( 0, 14 ) ) - 7;
4503 const int32_t offsetY = static_cast<int32_t>( Rand::Get( 0, 14 ) ) - 7;
4504 const fheroes2::Rect initialArea( area );
4505 fheroes2::Rect original = initialArea ^ fheroes2::Rect( area.x + offsetX, area.y + offsetY, area.width, area.height );
4506
4507 fheroes2::Rect shifted( initialArea.x - original.x, initialArea.y - original.y, original.width, original.height );
4508 if ( shifted.x < 0 ) {
4509 const int32_t offset = -shifted.x;
4510 shifted.x = 0;
4511 original.x += offset;
4512 shifted.width -= offset;
4513 shifted.x = 0;
4514 }
4515 if ( shifted.y < 0 ) {
4516 const int32_t offset = -shifted.y;
4517 shifted.y = 0;
4518 original.y += offset;
4519 shifted.height -= offset;
4520 shifted.y = 0;
4521 }
4522 fheroes2::Blit( spriteReddish, shifted.x, shifted.y, _mainSurface, original.x, original.y, shifted.width, shifted.height );
4523
4524 RedrawPartialFinish();
4525 }
4526 }
4527 }
4528
RedrawActionEarthQuakeSpell(const std::vector<int> & targets)4529 void Battle::Interface::RedrawActionEarthQuakeSpell( const std::vector<int> & targets )
4530 {
4531 Cursor & cursor = Cursor::Get();
4532 LocalEvent & le = LocalEvent::Get();
4533 fheroes2::Rect area = GetArea();
4534
4535 uint32_t frame = 0;
4536 area.height -= 38;
4537
4538 cursor.SetThemes( Cursor::WAR_POINTER );
4539
4540 fheroes2::Image sprite( area.width, area.height );
4541 fheroes2::Copy( _mainSurface, area.x, area.y, sprite, 0, 0, area.width, area.height );
4542
4543 _currentUnit = nullptr;
4544 AGG::PlaySound( M82::ERTHQUAK );
4545
4546 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
4547
4548 // draw earth quake
4549 while ( le.HandleEvents() && frame < 18 ) {
4550 CheckGlobalEvents( le );
4551
4552 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4553 const int32_t offsetX = static_cast<int32_t>( Rand::Get( 0, 14 ) ) - 7;
4554 const int32_t offsetY = static_cast<int32_t>( Rand::Get( 0, 14 ) ) - 7;
4555 const fheroes2::Rect initialArea( area );
4556 fheroes2::Rect original = initialArea ^ fheroes2::Rect( area.x + offsetX, area.y + offsetY, area.width, area.height );
4557
4558 fheroes2::Rect shifted( initialArea.x - original.x, initialArea.y - original.y, original.width, original.height );
4559 if ( shifted.x < 0 ) {
4560 const int32_t offset = -shifted.x;
4561 shifted.x = 0;
4562 original.x += offset;
4563 shifted.width -= offset;
4564 shifted.x = 0;
4565 }
4566 if ( shifted.y < 0 ) {
4567 const int32_t offset = -shifted.y;
4568 shifted.y = 0;
4569 original.y += offset;
4570 shifted.height -= offset;
4571 shifted.y = 0;
4572 }
4573
4574 fheroes2::Blit( sprite, shifted.x, shifted.y, _mainSurface, original.x, original.y, shifted.width, shifted.height );
4575
4576 RedrawPartialFinish();
4577 ++frame;
4578 }
4579 }
4580
4581 // draw cloud
4582 const int icn = ICN::LICHCLOD;
4583 frame = 0;
4584
4585 AGG::PlaySound( M82::CATSND02 );
4586
4587 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
4588
4589 while ( le.HandleEvents() && frame < fheroes2::AGG::GetICNCount( icn ) ) {
4590 CheckGlobalEvents( le );
4591
4592 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4593 RedrawPartialStart();
4594
4595 for ( std::vector<int>::const_iterator it = targets.begin(); it != targets.end(); ++it ) {
4596 fheroes2::Point pt2 = Catapult::GetTargetPosition( *it, true );
4597
4598 pt2.x += area.x;
4599 pt2.y += area.y;
4600
4601 const fheroes2::Sprite & spriteCloud = fheroes2::AGG::GetICN( icn, frame );
4602 fheroes2::Blit( spriteCloud, _mainSurface, pt2.x + spriteCloud.x(), pt2.y + spriteCloud.y() );
4603 }
4604
4605 RedrawPartialFinish();
4606
4607 ++frame;
4608 }
4609 }
4610 }
4611
RedrawActionRemoveMirrorImage(const std::vector<Unit * > & mirrorImages)4612 void Battle::Interface::RedrawActionRemoveMirrorImage( const std::vector<Unit *> & mirrorImages )
4613 {
4614 if ( mirrorImages.empty() ) // nothing to animate
4615 return;
4616
4617 LocalEvent & le = LocalEvent::Get();
4618
4619 int frame = 10;
4620 while ( le.HandleEvents() && frame > 0 ) {
4621 CheckGlobalEvents( le );
4622
4623 if ( Game::validateAnimationDelay( Game::BATTLE_FRAME_DELAY ) ) {
4624 const uint32_t alpha = static_cast<uint32_t>( frame ) * 25;
4625 for ( std::vector<Unit *>::const_iterator it = mirrorImages.begin(); it != mirrorImages.end(); ++it ) {
4626 if ( *it )
4627 ( *it )->SetCustomAlpha( alpha );
4628 }
4629
4630 Redraw();
4631
4632 --frame;
4633 }
4634 }
4635 status.SetMessage( _( "The mirror image is destroyed!" ), true );
4636 }
4637
RedrawTargetsWithFrameAnimation(int32_t dst,const TargetsInfo & targets,int icn,int m82,int repeatCount)4638 void Battle::Interface::RedrawTargetsWithFrameAnimation( int32_t dst, const TargetsInfo & targets, int icn, int m82, int repeatCount )
4639 {
4640 LocalEvent & le = LocalEvent::Get();
4641
4642 uint32_t frame = 0;
4643 const fheroes2::Rect & center = Board::GetCell( dst )->GetPos();
4644
4645 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
4646
4647 _currentUnit = nullptr;
4648 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4649 if ( ( *it ).defender && ( *it ).damage )
4650 ( *it ).defender->SwitchAnimation( Monster_Info::WNCE );
4651
4652 if ( M82::UNKNOWN != m82 )
4653 AGG::PlaySound( m82 );
4654
4655 uint32_t frameCount = fheroes2::AGG::GetICNCount( icn );
4656
4657 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
4658
4659 while ( le.HandleEvents() && frame < frameCount ) {
4660 CheckGlobalEvents( le );
4661
4662 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4663 RedrawPartialStart();
4664
4665 const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( icn, frame );
4666 fheroes2::Blit( sprite, _mainSurface, center.x + center.width / 2 + sprite.x(), center.y + center.height / 2 + sprite.y() );
4667 RedrawPartialFinish();
4668
4669 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4670 if ( ( *it ).defender && ( *it ).damage )
4671 ( *it ).defender->IncreaseAnimFrame( false );
4672 ++frame;
4673
4674 if ( frame == frameCount && repeatCount > 0 ) {
4675 --repeatCount;
4676 frame = 0;
4677 }
4678 }
4679 }
4680
4681 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4682 if ( ( *it ).defender ) {
4683 ( *it ).defender->SwitchAnimation( Monster_Info::STATIC );
4684 _currentUnit = nullptr;
4685 }
4686 }
4687
CalculateSpellPosition(const Battle::Unit & target,int spellICN,const fheroes2::Sprite & spellSprite)4688 fheroes2::Point CalculateSpellPosition( const Battle::Unit & target, int spellICN, const fheroes2::Sprite & spellSprite )
4689 {
4690 const fheroes2::Rect & pos = target.GetRectPosition();
4691 const fheroes2::Sprite & unitSprite = fheroes2::AGG::GetICN( target.GetMonsterSprite(), target.GetFrame() );
4692
4693 // Bottom-left corner (default) position with spell offset applied
4694 fheroes2::Point result( pos.x + spellSprite.x(), pos.y + pos.height + cellYOffset + spellSprite.y() );
4695
4696 switch ( spellICN ) {
4697 case ICN::SHIELD:
4698 // in front of the unit
4699 result.x += target.isReflect() ? -pos.width / ( target.isWide() ? 2 : 1 ) : pos.width;
4700 result.y += unitSprite.y() / 2;
4701 break;
4702 case ICN::BLIND: {
4703 // unit's eyes
4704 const fheroes2::Point & offset = target.animation.getBlindOffset();
4705
4706 // calculate OG Heroes2 unit position to apply offset to
4707 const int rearCenterX = ( target.isWide() && target.isReflect() ) ? pos.width * 3 / 4 : CELLW / 2;
4708
4709 // Overwrite result with custom blind value
4710 result.x += rearCenterX + ( target.isReflect() ? -offset.x : offset.x );
4711 result.y += offset.y;
4712 break;
4713 }
4714 case ICN::STONSKIN:
4715 case ICN::STELSKIN:
4716 // bottom center point
4717 result.x += pos.width / 2;
4718 break;
4719 default:
4720 // center point of the unit
4721 result.x += pos.width / 2;
4722 result.y += unitSprite.y() / 2;
4723 break;
4724 }
4725
4726 if ( result.y < 0 ) {
4727 const int maximumY = fheroes2::AGG::GetAbsoluteICNHeight( spellICN );
4728 result.y = maximumY + spellSprite.y();
4729 }
4730
4731 return result;
4732 }
4733
RedrawTargetsWithFrameAnimation(const TargetsInfo & targets,int icn,int m82,bool wnce)4734 void Battle::Interface::RedrawTargetsWithFrameAnimation( const TargetsInfo & targets, int icn, int m82, bool wnce )
4735 {
4736 LocalEvent & le = LocalEvent::Get();
4737
4738 uint32_t frame = 0;
4739
4740 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
4741
4742 _currentUnit = nullptr;
4743
4744 if ( wnce )
4745 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4746 if ( ( *it ).defender && ( *it ).damage )
4747 ( *it ).defender->SwitchAnimation( Monster_Info::WNCE );
4748
4749 if ( M82::UNKNOWN != m82 )
4750 AGG::PlaySound( m82 );
4751
4752 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
4753
4754 while ( le.HandleEvents() && frame < fheroes2::AGG::GetICNCount( icn ) ) {
4755 CheckGlobalEvents( le );
4756
4757 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4758 RedrawPartialStart();
4759
4760 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4761 if ( ( *it ).defender ) {
4762 const bool reflect = ( icn == ICN::SHIELD && it->defender->isReflect() );
4763 const fheroes2::Sprite & spellSprite = fheroes2::AGG::GetICN( icn, frame );
4764 const fheroes2::Point & pos = CalculateSpellPosition( *it->defender, icn, spellSprite );
4765 fheroes2::Blit( spellSprite, _mainSurface, pos.x, pos.y, reflect );
4766 }
4767 RedrawPartialFinish();
4768
4769 if ( wnce )
4770 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4771 if ( ( *it ).defender && ( *it ).damage )
4772 ( *it ).defender->IncreaseAnimFrame( false );
4773 ++frame;
4774 }
4775 }
4776
4777 if ( wnce )
4778 for ( TargetsInfo::const_iterator it = targets.begin(); it != targets.end(); ++it )
4779 if ( ( *it ).defender ) {
4780 ( *it ).defender->SwitchAnimation( Monster_Info::STATIC );
4781 _currentUnit = nullptr;
4782 }
4783 }
4784
RedrawTroopWithFrameAnimation(Unit & b,int icn,int m82,CreatueSpellAnimation animation)4785 void Battle::Interface::RedrawTroopWithFrameAnimation( Unit & b, int icn, int m82, CreatueSpellAnimation animation )
4786 {
4787 LocalEvent & le = LocalEvent::Get();
4788
4789 uint32_t frame = 0;
4790 const bool reflect = ( icn == ICN::SHIELD && b.isReflect() );
4791
4792 Cursor::Get().SetThemes( Cursor::WAR_POINTER );
4793
4794 if ( animation == WINCE ) {
4795 _currentUnit = nullptr;
4796 b.SwitchAnimation( Monster_Info::WNCE );
4797 }
4798 else if ( animation == RESURRECT ) {
4799 _currentUnit = nullptr;
4800 b.SwitchAnimation( Monster_Info::KILL, true );
4801 }
4802
4803 if ( M82::UNKNOWN != m82 )
4804 AGG::PlaySound( m82 );
4805
4806 Game::passAnimationDelay( Game::BATTLE_SPELL_DELAY );
4807
4808 while ( le.HandleEvents() && frame < fheroes2::AGG::GetICNCount( icn ) ) {
4809 CheckGlobalEvents( le );
4810
4811 if ( Game::validateAnimationDelay( Game::BATTLE_SPELL_DELAY ) ) {
4812 RedrawPartialStart();
4813
4814 const fheroes2::Sprite & spellSprite = fheroes2::AGG::GetICN( icn, frame );
4815 const fheroes2::Point & pos = CalculateSpellPosition( b, icn, spellSprite );
4816 fheroes2::Blit( spellSprite, _mainSurface, pos.x, pos.y, reflect );
4817 RedrawPartialFinish();
4818
4819 if ( animation != NONE ) {
4820 if ( animation == RESURRECT ) {
4821 if ( b.isFinishAnimFrame() )
4822 b.SwitchAnimation( Monster_Info::STATIC );
4823 }
4824 b.IncreaseAnimFrame( false );
4825 }
4826 ++frame;
4827 }
4828 }
4829
4830 if ( animation != NONE ) {
4831 b.SwitchAnimation( Monster_Info::STATIC );
4832 _currentUnit = nullptr;
4833 }
4834 }
4835
RedrawBridgeAnimation(const bool bridgeDownAnimation)4836 void Battle::Interface::RedrawBridgeAnimation( const bool bridgeDownAnimation )
4837 {
4838 LocalEvent & le = LocalEvent::Get();
4839
4840 _bridgeAnimation.animationIsRequired = true;
4841
4842 _bridgeAnimation.currentFrameId = bridgeDownAnimation ? BridgeMovementAnimation::UP_POSITION : BridgeMovementAnimation::DOWN_POSITION;
4843
4844 if ( bridgeDownAnimation )
4845 AGG::PlaySound( M82::DRAWBRG );
4846
4847 while ( le.HandleEvents() ) {
4848 if ( bridgeDownAnimation ) {
4849 if ( _bridgeAnimation.currentFrameId < BridgeMovementAnimation::DOWN_POSITION )
4850 break;
4851 }
4852 else {
4853 if ( _bridgeAnimation.currentFrameId > BridgeMovementAnimation::UP_POSITION )
4854 break;
4855 }
4856
4857 CheckGlobalEvents( le );
4858
4859 if ( Game::validateAnimationDelay( Game::BATTLE_BRIDGE_DELAY ) ) {
4860 Redraw();
4861
4862 if ( bridgeDownAnimation )
4863 --_bridgeAnimation.currentFrameId;
4864 else
4865 ++_bridgeAnimation.currentFrameId;
4866 }
4867 }
4868
4869 _bridgeAnimation.animationIsRequired = false;
4870
4871 if ( !bridgeDownAnimation )
4872 AGG::PlaySound( M82::DRAWBRG );
4873 }
4874
IdleTroopsAnimation(void)4875 bool Battle::Interface::IdleTroopsAnimation( void )
4876 {
4877 if ( Game::validateAnimationDelay( Game::BATTLE_IDLE_DELAY ) ) {
4878 const bool redrawNeeded = arena.GetForce1().animateIdleUnits();
4879 return arena.GetForce2().animateIdleUnits() || redrawNeeded;
4880 }
4881
4882 return false;
4883 }
4884
ResetIdleTroopAnimation(void)4885 void Battle::Interface::ResetIdleTroopAnimation( void )
4886 {
4887 arena.GetForce1().resetIdleAnimation();
4888 arena.GetForce2().resetIdleAnimation();
4889 }
4890
CheckGlobalEvents(LocalEvent & le)4891 void Battle::Interface::CheckGlobalEvents( LocalEvent & le )
4892 {
4893 if ( Game::validateAnimationDelay( Game::BATTLE_SELECTED_UNIT_DELAY ) )
4894 UpdateContourColor();
4895
4896 // animate heroes
4897 if ( Game::validateAnimationDelay( Game::BATTLE_OPPONENTS_DELAY ) ) {
4898 if ( opponent1 )
4899 opponent1->Update();
4900
4901 if ( opponent2 )
4902 opponent2->Update();
4903
4904 humanturn_redraw = true;
4905 }
4906
4907 // flags animation
4908 if ( Game::validateAnimationDelay( Game::BATTLE_FLAGS_DELAY ) ) {
4909 ++animation_flags_frame;
4910 humanturn_redraw = true;
4911 }
4912
4913 // break auto battle
4914 if ( arena.CanBreakAutoBattle()
4915 && ( le.MouseClickLeft( btn_auto.area() )
4916 || ( le.KeyPress()
4917 && ( Game::HotKeyPressEvent( Game::EVENT_BATTLE_AUTOSWITCH )
4918 || ( Game::HotKeyPressEvent( Game::EVENT_BATTLE_RETREAT )
4919 && Dialog::YES == Dialog::Message( "", _( "Break auto battle?" ), Font::BIG, Dialog::YES | Dialog::NO ) ) ) ) ) ) {
4920 arena.BreakAutoBattle();
4921 }
4922 }
4923
ProcessingHeroDialogResult(int res,Actions & a)4924 void Battle::Interface::ProcessingHeroDialogResult( int res, Actions & a )
4925 {
4926 switch ( res ) {
4927 // cast
4928 case 1: {
4929 const HeroBase * hero = _currentUnit->GetCurrentOrArmyCommander();
4930
4931 if ( hero ) {
4932 if ( hero->HaveSpellBook() ) {
4933 std::string msg;
4934 if ( arena.isDisableCastSpell( Spell::NONE, &msg ) )
4935 Dialog::Message( "", msg, Font::BIG, Dialog::OK );
4936 else {
4937 std::function<void( const std::string & )> statusCallback = [this]( const std::string & statusStr ) {
4938 status.SetMessage( statusStr );
4939 status.Redraw();
4940 };
4941
4942 const Spell spell = hero->OpenSpellBook( SpellBook::Filter::CMBT, true, &statusCallback );
4943 if ( spell.isValid() ) {
4944 std::string error;
4945
4946 if ( arena.isDisableCastSpell( spell, &msg ) )
4947 Dialog::Message( "", msg, Font::BIG, Dialog::OK );
4948 else if ( hero->CanCastSpell( spell, &error ) ) {
4949 if ( spell.isApplyWithoutFocusObject() ) {
4950 a.push_back( Command( CommandType::MSG_BATTLE_CAST, spell.GetID(), -1 ) );
4951 humanturn_redraw = true;
4952 humanturn_exit = true;
4953 }
4954 else
4955 humanturn_spell = spell;
4956 }
4957 else if ( !error.empty() )
4958 Dialog::Message( _( "Error" ), error, Font::BIG, Dialog::OK );
4959 }
4960 }
4961 }
4962 else
4963 Dialog::Message( "", _( "No spells to cast." ), Font::BIG, Dialog::OK );
4964 }
4965 break;
4966 }
4967
4968 // retreat
4969 case 2: {
4970 if ( arena.CanRetreatOpponent( _currentUnit->GetCurrentOrArmyColor() ) ) {
4971 if ( Dialog::YES == Dialog::Message( "", _( "Are you sure you want to retreat?" ), Font::BIG, Dialog::YES | Dialog::NO ) ) {
4972 a.push_back( Command( CommandType::MSG_BATTLE_RETREAT ) );
4973 a.push_back( Command( CommandType::MSG_BATTLE_END_TURN, _currentUnit->GetUID() ) );
4974 humanturn_exit = true;
4975 }
4976 }
4977 else {
4978 Dialog::Message( "", _( "Retreat disabled" ), Font::BIG, Dialog::OK );
4979 }
4980 break;
4981 }
4982
4983 // surrender
4984 case 3: {
4985 if ( arena.CanSurrenderOpponent( _currentUnit->GetCurrentOrArmyColor() ) ) {
4986 const HeroBase * enemy = arena.getEnemyCommander( arena.GetCurrentColor() );
4987
4988 if ( enemy ) {
4989 const s32 cost = arena.GetCurrentForce().GetSurrenderCost();
4990 Kingdom & kingdom = world.GetKingdom( arena.GetCurrentColor() );
4991
4992 if ( DialogBattleSurrender( *enemy, cost, kingdom ) ) {
4993 a.push_back( Command( CommandType::MSG_BATTLE_SURRENDER ) );
4994 a.push_back( Command( CommandType::MSG_BATTLE_END_TURN, _currentUnit->GetUID() ) );
4995 humanturn_exit = true;
4996 }
4997 }
4998 }
4999 else {
5000 Dialog::Message( "", _( "Surrender disabled" ), Font::BIG, Dialog::OK );
5001 }
5002 break;
5003 }
5004
5005 default:
5006 break;
5007 }
5008 }
5009
PopupDamageInfo()5010 Battle::PopupDamageInfo::PopupDamageInfo()
5011 : Dialog::FrameBorder( 5 )
5012 , _cell( nullptr )
5013 , _attacker( nullptr )
5014 , _defender( nullptr )
5015 , _redraw( false )
5016 {}
5017
SetInfo(const Cell * cell,const Unit * attacker,const Unit * defender,const fheroes2::Point & offset)5018 void Battle::PopupDamageInfo::SetInfo( const Cell * cell, const Unit * attacker, const Unit * defender, const fheroes2::Point & offset )
5019 {
5020 if ( cell == nullptr || attacker == nullptr || defender == nullptr ) {
5021 return;
5022 }
5023
5024 if ( !Settings::Get().ExtBattleShowDamage() || !Game::validateAnimationDelay( Game::BATTLE_POPUP_DELAY ) ) {
5025 return;
5026 }
5027
5028 _redraw = true;
5029 _cell = cell;
5030 _attacker = attacker;
5031 _defender = defender;
5032
5033 const fheroes2::Rect & rt = cell->GetPos();
5034 SetPosition( rt.x + rt.width + offset.x, rt.y + offset.y, 20, 20 );
5035 }
5036
Reset(void)5037 void Battle::PopupDamageInfo::Reset( void )
5038 {
5039 if ( _redraw ) {
5040 restorer.restore();
5041 _redraw = false;
5042 _cell = nullptr;
5043 _attacker = nullptr;
5044 _defender = nullptr;
5045 }
5046 Game::AnimateResetDelay( Game::BATTLE_POPUP_DELAY );
5047 }
5048
Redraw(int maxw,int)5049 void Battle::PopupDamageInfo::Redraw( int maxw, int /*maxh*/ )
5050 {
5051 if ( _redraw ) {
5052 uint32_t tmp1 = _attacker->CalculateMinDamage( *_defender );
5053 uint32_t tmp2 = _attacker->CalculateMaxDamage( *_defender );
5054
5055 if ( _attacker->Modes( SP_BLESS ) )
5056 tmp1 = tmp2;
5057 else if ( _attacker->Modes( SP_CURSE ) )
5058 tmp2 = tmp1;
5059
5060 std::string str = tmp1 == tmp2 ? _( "Damage: %{max}" ) : _( "Damage: %{min} - %{max}" );
5061
5062 StringReplace( str, "%{min}", tmp1 );
5063 StringReplace( str, "%{max}", tmp2 );
5064
5065 Text text1( str, Font::SMALL );
5066
5067 tmp1 = _defender->HowManyWillKilled( tmp1 );
5068 tmp2 = _defender->HowManyWillKilled( tmp2 );
5069
5070 if ( tmp1 > _defender->GetCount() )
5071 tmp1 = _defender->GetCount();
5072 if ( tmp2 > _defender->GetCount() )
5073 tmp2 = _defender->GetCount();
5074
5075 str = tmp1 == tmp2 ? _( "Perish: %{max}" ) : _( "Perish: %{min} - %{max}" );
5076
5077 StringReplace( str, "%{min}", tmp1 );
5078 StringReplace( str, "%{max}", tmp2 );
5079
5080 Text text2( str, Font::SMALL );
5081
5082 int tw = 5 + ( text1.w() > text2.w() ? text1.w() : text2.w() );
5083 int th = ( text1.h() + text2.h() );
5084
5085 const fheroes2::Rect & borderArea = GetArea();
5086 const fheroes2::Rect & borderRect = GetRect();
5087 const fheroes2::Rect & pos = _cell->GetPos();
5088
5089 int tx = borderRect.x;
5090 int ty = borderRect.y;
5091
5092 if ( borderRect.x + borderRect.width > maxw ) {
5093 tx = maxw - borderRect.width - 5;
5094 ty = pos.y - pos.height;
5095 }
5096
5097 if ( borderRect.x != tx || borderRect.y != ty || borderArea.width != tw || borderArea.height != th ) {
5098 SetPosition( tx, ty, tw, th );
5099 }
5100
5101 const fheroes2::Rect & currectArea = GetRect();
5102 Dialog::FrameBorder::RenderOther( fheroes2::AGG::GetICN( ICN::CELLWIN, 1 ), currectArea );
5103
5104 text1.Blit( borderArea.x, borderArea.y );
5105 text2.Blit( borderArea.x, borderArea.y + borderArea.height / 2 );
5106 }
5107 }
5108