1 /***************************************************************************
2  *   Copyright (C) 2009 by Andrey Afletdinov <fheroes2@gmail.com>          *
3  *                                                                         *
4  *   Part of the Free Heroes2 Engine:                                      *
5  *   http://sourceforge.net/projects/fheroes2                              *
6  *                                                                         *
7  *   This program is free software; you can redistribute it and/or modify  *
8  *   it under the terms of the GNU General Public License as published by  *
9  *   the Free Software Foundation; either version 2 of the License, or     *
10  *   (at your option) any later version.                                   *
11  *                                                                         *
12  *   This program is distributed in the hope that it will be useful,       *
13  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
14  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
15  *   GNU General Public License for more details.                          *
16  *                                                                         *
17  *   You should have received a copy of the GNU General Public License     *
18  *   along with this program; if not, write to the                         *
19  *   Free Software Foundation, Inc.,                                       *
20  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
21  ***************************************************************************/
22 
23 #include <algorithm>
24 #include <atomic>
25 #include <cassert>
26 #include <cmath>
27 
28 #include "agg.h"
29 #include "audio.h"
30 #include "cursor.h"
31 #include "difficulty.h"
32 #include "game.h"
33 #include "game_credits.h"
34 #include "game_delays.h"
35 #include "game_interface.h"
36 #include "game_static.h"
37 #include "icn.h"
38 #include "m82.h"
39 #include "maps_tiles.h"
40 #include "monster.h"
41 #include "mp2.h"
42 #include "rand.h"
43 #include "save_format_version.h"
44 #include "settings.h"
45 #include "skill.h"
46 #include "text.h"
47 #include "tools.h"
48 #include "translations.h"
49 #include "world.h"
50 
51 namespace
52 {
53     std::string lastMapFileName;
54     std::vector<Player> savedPlayers;
55 
56     int save_version = CURRENT_FORMAT_VERSION;
57 
58     std::string last_name;
59 
60     bool updateSoundsOnFocusUpdate = true;
61     std::atomic<int> currentMusic{ MUS::UNKNOWN };
62 
63     u32 maps_animation_frame = 0;
64 
65     std::vector<int> reserved_vols( LOOPXX_COUNT, 0 );
66 
GetMixerChannelFromObject(const Maps::Tiles & tile)67     uint32_t GetMixerChannelFromObject( const Maps::Tiles & tile )
68     {
69         // check stream first
70         if ( tile.isStream() ) {
71             return 13;
72         }
73 
74         return M82::GetIndexLOOP00XXFromObject( tile.GetObject( false ) );
75     }
76 }
77 
78 namespace Game
79 {
80     void AnimateDelaysInitialize( void );
81     void KeyboardGlobalFilter( int, int );
82 
83     void HotKeysDefaults( void );
84     void HotKeysLoad( const std::string & );
85 
86     namespace ObjectFadeAnimation
87     {
FadeTask(MP2::MapObjectType object_,uint32_t objectIndex_,uint32_t animationIndex_,int32_t fromIndex_,int32_t toIndex_,uint8_t alpha_,bool fadeOut_,bool fadeIn_,uint8_t objectTileset_)88         FadeTask::FadeTask( MP2::MapObjectType object_, uint32_t objectIndex_, uint32_t animationIndex_, int32_t fromIndex_, int32_t toIndex_, uint8_t alpha_,
89                             bool fadeOut_, bool fadeIn_, uint8_t objectTileset_ )
90             : object( object_ )
91             , objectIndex( objectIndex_ )
92             , animationIndex( animationIndex_ )
93             , fromIndex( fromIndex_ )
94             , toIndex( toIndex_ )
95             , alpha( alpha_ )
96             , fadeOut( fadeOut_ )
97             , fadeIn( fadeIn_ )
98             , objectTileset( objectTileset_ )
99         {}
100 
FadeTask()101         FadeTask::FadeTask()
102             : object( MP2::OBJ_ZERO )
103             , objectIndex( 0 )
104             , animationIndex( 0 )
105             , fromIndex( 0 )
106             , toIndex( 0 )
107             , alpha( 0 )
108             , fadeOut( false )
109             , fadeIn( false )
110             , objectTileset( 0 )
111         {}
112 
113         // Single instance of FadeTask.
114         FadeTask fadeTask;
115     }
116 }
117 
118 // Returns the difficulty level based on the type of game.
getDifficulty()119 int Game::getDifficulty()
120 {
121     const Settings & configuration = Settings::Get();
122 
123     return ( configuration.isCampaignGameType() ? configuration.CurrentFileInfo().difficulty : configuration.GameDifficulty() );
124 }
125 
LoadPlayers(const std::string & mapFileName,Players & players)126 void Game::LoadPlayers( const std::string & mapFileName, Players & players )
127 {
128     if ( lastMapFileName != mapFileName || savedPlayers.size() != players.size() ) {
129         return;
130     }
131 
132     const auto newHumanCount = std::count_if( players.begin(), players.end(), []( const Player * player ) { return player->GetControl() == CONTROL_HUMAN; } );
133     const auto savedHumanCount = std::count_if( savedPlayers.begin(), savedPlayers.end(), []( const Player & player ) { return player.GetControl() == CONTROL_HUMAN; } );
134 
135     if ( newHumanCount != savedHumanCount ) {
136         return;
137     }
138 
139     players.clear();
140     for ( const Player & p : savedPlayers ) {
141         Player * player = new Player( p.GetColor() );
142         player->SetRace( p.GetRace() );
143         player->SetControl( p.GetControl() );
144         player->SetFriends( p.GetFriends() );
145         player->SetName( p.GetName() );
146         players.push_back( player );
147         Players::Set( Color::GetIndex( p.GetColor() ), player );
148     }
149 }
150 
saveDifficulty(const int difficulty)151 void Game::saveDifficulty( const int difficulty )
152 {
153     Settings::Get().SetGameDifficulty( difficulty );
154 }
155 
SavePlayers(const std::string & mapFileName,const Players & players)156 void Game::SavePlayers( const std::string & mapFileName, const Players & players )
157 {
158     lastMapFileName = mapFileName;
159     savedPlayers.clear();
160     for ( const Player * p : players ) {
161         Player player( p->GetColor() );
162         player.SetRace( p->GetRace() );
163         player.SetControl( p->GetControl() );
164         player.SetFriends( p->GetFriends() );
165         player.SetName( p->GetName() );
166         savedPlayers.push_back( player );
167     }
168 }
169 
SetLoadVersion(int ver)170 void Game::SetLoadVersion( int ver )
171 {
172     save_version = ver;
173 }
174 
GetLoadVersion(void)175 int Game::GetLoadVersion( void )
176 {
177     return save_version;
178 }
179 
GetLastSavename(void)180 const std::string & Game::GetLastSavename( void )
181 {
182     return last_name;
183 }
184 
SetLastSavename(const std::string & name)185 void Game::SetLastSavename( const std::string & name )
186 {
187     last_name = name;
188 }
189 
Credits()190 fheroes2::GameMode Game::Credits()
191 {
192     ShowCredits();
193 
194     return fheroes2::GameMode::MAIN_MENU;
195 }
196 
UpdateSoundsOnFocusUpdate()197 bool Game::UpdateSoundsOnFocusUpdate()
198 {
199     return updateSoundsOnFocusUpdate;
200 }
201 
SetUpdateSoundsOnFocusUpdate(bool update)202 void Game::SetUpdateSoundsOnFocusUpdate( bool update )
203 {
204     updateSoundsOnFocusUpdate = update;
205 }
206 
Init(void)207 void Game::Init( void )
208 {
209     // default events
210     LocalEvent::SetStateDefaults();
211 
212     // set global events
213     LocalEvent & le = LocalEvent::Get();
214     le.SetGlobalFilterMouseEvents( Cursor::Redraw );
215     le.SetGlobalFilterKeysEvents( Game::KeyboardGlobalFilter );
216 
217     Game::AnimateDelaysInitialize();
218 
219     HotKeysDefaults();
220 
221     const std::string hotkeys = Settings::GetLastFile( "", "fheroes2.key" );
222     Game::HotKeysLoad( hotkeys );
223 }
224 
CurrentMusic()225 int Game::CurrentMusic()
226 {
227     return currentMusic;
228 }
229 
SetCurrentMusic(const int mus)230 void Game::SetCurrentMusic( const int mus )
231 {
232     currentMusic = mus;
233 }
234 
PrepareFadeTask(const MP2::MapObjectType objectType,int32_t fromIndex,int32_t toIndex,bool fadeOut,bool fadeIn)235 void Game::ObjectFadeAnimation::PrepareFadeTask( const MP2::MapObjectType objectType, int32_t fromIndex, int32_t toIndex, bool fadeOut, bool fadeIn )
236 {
237     const uint8_t alpha = fadeOut ? 255u : 0;
238     const Maps::Tiles & fromTile = world.GetTiles( fromIndex );
239 
240     if ( objectType == MP2::OBJ_ZERO ) {
241         fadeTask = FadeTask();
242     }
243     else if ( objectType == MP2::OBJ_MONSTER ) {
244         const auto & spriteIndicies = Maps::Tiles::GetMonsterSpriteIndices( fromTile, fromTile.QuantityMonster().GetSpriteIndex() );
245 
246         fadeTask = FadeTask( objectType, spriteIndicies.first, spriteIndicies.second, fromIndex, toIndex, alpha, fadeOut, fadeIn, 0 );
247     }
248     else if ( objectType == MP2::OBJ_BOAT ) {
249         fadeTask = FadeTask( objectType, fromTile.GetObjectSpriteIndex(), 0, fromIndex, toIndex, alpha, fadeOut, fadeIn, 0 );
250     }
251     else {
252         const int icn = MP2::GetICNObject( fromTile.GetObjectTileset() );
253         const uint32_t animationIndex = ICN::AnimationFrame( icn, fromTile.GetObjectSpriteIndex(), Game::MapsAnimationFrame(), fromTile.GetQuantity2() != 0 );
254 
255         fadeTask = FadeTask( objectType, fromTile.GetObjectSpriteIndex(), animationIndex, fromIndex, toIndex, alpha, fadeOut, fadeIn, fromTile.GetObjectTileset() );
256     }
257 }
258 
PerformFadeTask()259 void Game::ObjectFadeAnimation::PerformFadeTask()
260 {
261     auto removeObject = []() {
262         Maps::Tiles & tile = world.GetTiles( fadeTask.fromIndex );
263 
264         if ( tile.GetObject() == fadeTask.object ) {
265             tile.RemoveObjectSprite();
266             tile.setAsEmpty();
267         }
268     };
269     auto addObject = []() {
270         Maps::Tiles & tile = world.GetTiles( fadeTask.toIndex );
271 
272         if ( tile.GetObject() != fadeTask.object && fadeTask.object == MP2::OBJ_BOAT ) {
273             tile.setBoat( Direction::RIGHT );
274         }
275     };
276     auto redrawGameArea = []() {
277         fheroes2::Display & display = fheroes2::Display::instance();
278         Interface::GameArea & gameArea = Interface::Basic::Get().GetGameArea();
279 
280         gameArea.Redraw( display, Interface::LEVEL_ALL );
281 
282         display.render();
283     };
284 
285     LocalEvent & le = LocalEvent::Get();
286 
287     while ( le.HandleEvents() && ( fadeTask.fadeOut || fadeTask.fadeIn ) ) {
288         if ( Game::validateAnimationDelay( Game::HEROES_PICKUP_DELAY ) ) {
289             if ( fadeTask.fadeOut ) {
290                 if ( fadeTask.alpha > 20 ) {
291                     fadeTask.alpha -= 20;
292                 }
293                 else {
294                     removeObject();
295 
296                     if ( fadeTask.fadeIn ) {
297                         fadeTask.fadeOut = false;
298                         fadeTask.alpha = 0;
299                     }
300                     else {
301                         fadeTask = FadeTask();
302                     }
303                 }
304             }
305             else if ( fadeTask.fadeIn ) {
306                 if ( fadeTask.alpha == 0 ) {
307                     addObject();
308                 }
309 
310                 if ( fadeTask.alpha < 235 ) {
311                     fadeTask.alpha += 20;
312                 }
313                 else {
314                     fadeTask = FadeTask();
315                 }
316             }
317 
318             redrawGameArea();
319         }
320     }
321 
322     if ( fadeTask.fadeOut ) {
323         removeObject();
324     }
325 
326     if ( fadeTask.fadeIn ) {
327         addObject();
328     }
329 
330     fadeTask = FadeTask();
331 
332     redrawGameArea();
333 }
334 
GetFadeTask()335 const Game::ObjectFadeAnimation::FadeTask & Game::ObjectFadeAnimation::GetFadeTask()
336 {
337     return fadeTask;
338 }
339 
MapsAnimationFrame(void)340 u32 & Game::MapsAnimationFrame( void )
341 {
342     return maps_animation_frame;
343 }
344 
345 // play environment sounds from the game area in focus
EnvironmentSoundMixer()346 void Game::EnvironmentSoundMixer()
347 {
348     size_t availableChannels = Mixer::getChannelCount();
349     if ( availableChannels == 0 ) {
350         return;
351     }
352 
353     const fheroes2::Point abs_pt( Interface::GetFocusCenter() );
354     std::fill( reserved_vols.begin(), reserved_vols.end(), 0 );
355 
356     const int32_t maxOffset = 3;
357 
358     // Get all valid positions within 7 x 7 area.
359     std::vector<fheroes2::Point> positions;
360     for ( int32_t y = -maxOffset; y <= maxOffset; ++y ) {
361         for ( int32_t x = -maxOffset; x <= maxOffset; ++x ) {
362             if ( Maps::isValidAbsPoint( x + abs_pt.x, y + abs_pt.y ) ) {
363                 positions.emplace_back( x, y );
364             }
365         }
366     }
367 
368     // Sort positions by distance to the center.
369     std::stable_sort( positions.begin(), positions.end(),
370                       []( const fheroes2::Point & p1, const fheroes2::Point & p2 ) { return p1.x * p1.x + p1.y * p1.y < p2.x * p2.x + p2.y * p2.y; } );
371 
372     const double maxDistance = std::sqrt( maxOffset * maxOffset + maxOffset * maxOffset );
373     double maxVolume = Mixer::MaxVolume();
374     double minVolumeOnMaxDistance = maxVolume * 0.1; // 10% from maximum volume
375 
376     maxVolume -= minVolumeOnMaxDistance; // need to remove these 10% from max value as we're going to add it later
377     minVolumeOnMaxDistance += 0.5; // this is done to make casting faster. We know that the value is always positive.
378 
379     for ( const fheroes2::Point & pos : positions ) {
380         const uint32_t channel = GetMixerChannelFromObject( world.GetTiles( pos.x + abs_pt.x, pos.y + abs_pt.y ) );
381         if ( channel < reserved_vols.size() ) {
382             const double distance = std::sqrt( pos.x * pos.x + pos.y * pos.y );
383             const int32_t volume = static_cast<int32_t>( ( ( maxDistance - distance ) / maxDistance ) * maxVolume + minVolumeOnMaxDistance );
384 
385             if ( reserved_vols[channel] == 0 ) {
386                 if ( availableChannels == 0 ) {
387                     // No new channel can be added.
388                     continue;
389                 }
390                 --availableChannels;
391             }
392 
393             if ( volume > reserved_vols[channel] ) {
394                 reserved_vols[channel] = volume;
395             }
396         }
397     }
398 
399     AGG::LoadLOOPXXSounds( reserved_vols, true );
400 }
401 
restoreSoundsForCurrentFocus()402 void Game::restoreSoundsForCurrentFocus()
403 {
404     AGG::ResetMixer();
405 
406     switch ( Interface::GetFocusType() ) {
407     case GameFocus::HEROES: {
408         const Heroes * focusedHero = Interface::GetFocusHeroes();
409         assert( focusedHero != nullptr );
410 
411         const int heroIndexPos = focusedHero->GetIndex();
412         if ( heroIndexPos >= 0 ) {
413             Game::EnvironmentSoundMixer();
414             AGG::PlayMusic( MUS::FromGround( world.GetTiles( heroIndexPos ).GetGround() ), true, true );
415         }
416         break;
417     }
418 
419     case GameFocus::CASTLE: {
420         const Castle * focusedCastle = Interface::GetFocusCastle();
421         assert( focusedCastle != nullptr );
422 
423         Game::EnvironmentSoundMixer();
424         AGG::PlayMusic( MUS::FromGround( world.GetTiles( focusedCastle->GetIndex() ).GetGround() ), true, true );
425         break;
426     }
427 
428     default:
429         break;
430     }
431 }
432 
GetRating(void)433 u32 Game::GetRating( void )
434 {
435     const Settings & conf = Settings::Get();
436     u32 rating = 50;
437 
438     switch ( conf.MapsDifficulty() ) {
439     case Difficulty::NORMAL:
440         rating += 20;
441         break;
442     case Difficulty::HARD:
443         rating += 40;
444         break;
445     case Difficulty::EXPERT:
446     case Difficulty::IMPOSSIBLE:
447         rating += 80;
448         break;
449     default:
450         break;
451     }
452 
453     switch ( Game::getDifficulty() ) {
454     case Difficulty::NORMAL:
455         rating += 30;
456         break;
457     case Difficulty::HARD:
458         rating += 50;
459         break;
460     case Difficulty::EXPERT:
461         rating += 70;
462         break;
463     case Difficulty::IMPOSSIBLE:
464         rating += 90;
465         break;
466     default:
467         break;
468     }
469 
470     return rating;
471 }
472 
GetGameOverScores(void)473 u32 Game::GetGameOverScores( void )
474 {
475     const Settings & conf = Settings::Get();
476 
477     uint32_t mapSizeFactor = 0;
478 
479     switch ( conf.MapsSize().width ) {
480     case Maps::SMALL:
481         mapSizeFactor = 140;
482         break;
483     case Maps::MEDIUM:
484         mapSizeFactor = 100;
485         break;
486     case Maps::LARGE:
487         mapSizeFactor = 80;
488         break;
489     case Maps::XLARGE:
490         mapSizeFactor = 60;
491         break;
492     }
493 
494     const uint32_t daysFactor = world.CountDay() * mapSizeFactor / 100;
495 
496     uint32_t daysScore = 0;
497 
498     if ( daysFactor <= 60 ) {
499         daysScore = daysFactor;
500     }
501     else if ( daysFactor <= 120 ) {
502         daysScore = daysFactor / 2 + 30;
503     }
504     else if ( daysFactor <= 360 ) {
505         daysScore = daysFactor / 4 + 60;
506     }
507     else if ( daysFactor <= 600 ) {
508         daysScore = daysFactor / 8 + 105;
509     }
510     else {
511         daysScore = 180;
512     }
513 
514     return GetRating() * ( 200 - daysScore ) / 100;
515 }
516 
ShowMapLoadingText(void)517 void Game::ShowMapLoadingText( void )
518 {
519     fheroes2::Display & display = fheroes2::Display::instance();
520     const fheroes2::Rect pos( 0, display.height() / 2, display.width(), display.height() / 2 );
521     TextBox text( _( "Map is loading..." ), Font::BIG, pos.width );
522 
523     // blit test
524     display.fill( 0 );
525     text.Blit( pos.x, pos.y );
526     display.render();
527 }
528 
GetLostTownDays(void)529 u32 Game::GetLostTownDays( void )
530 {
531     return GameStatic::GetGameOverLostDays();
532 }
533 
GetViewDistance(u32 d)534 u32 Game::GetViewDistance( u32 d )
535 {
536     return GameStatic::GetOverViewDistance( d );
537 }
538 
GetWhirlpoolPercent(void)539 u32 Game::GetWhirlpoolPercent( void )
540 {
541     return GameStatic::GetLostOnWhirlpoolPercent();
542 }
543 
GetKingdomColors(void)544 int Game::GetKingdomColors( void )
545 {
546     return Settings::Get().GetPlayers().GetColors();
547 }
548 
GetActualKingdomColors(void)549 int Game::GetActualKingdomColors( void )
550 {
551     return Settings::Get().GetPlayers().GetActualColors();
552 }
553 
CountScoute(uint32_t count,int scoute,bool shorts)554 std::string Game::CountScoute( uint32_t count, int scoute, bool shorts )
555 {
556     double infelicity = 0;
557     std::string res;
558 
559     switch ( scoute ) {
560     case Skill::Level::BASIC:
561         infelicity = count * 30 / 100.0;
562         break;
563 
564     case Skill::Level::ADVANCED:
565         infelicity = count * 15 / 100.0;
566         break;
567 
568     case Skill::Level::EXPERT:
569         res = shorts ? GetStringShort( count ) : std::to_string( count );
570         break;
571 
572     default:
573         return Army::SizeString( count );
574     }
575 
576     if ( res.empty() ) {
577         uint32_t min = Rand::Get( static_cast<uint32_t>( std::floor( count - infelicity + 0.5 ) ), static_cast<uint32_t>( std::floor( count + infelicity + 0.5 ) ) );
578         uint32_t max = 0;
579 
580         if ( min > count ) {
581             max = min;
582             min = static_cast<uint32_t>( std::floor( count - infelicity + 0.5 ) );
583         }
584         else
585             max = static_cast<uint32_t>( std::floor( count + infelicity + 0.5 ) );
586 
587         res = std::to_string( min );
588 
589         if ( min != max ) {
590             res.append( "-" );
591             res.append( std::to_string( max ) );
592         }
593     }
594 
595     return res;
596 }
597 
CountThievesGuild(uint32_t monsterCount,int guildCount)598 std::string Game::CountThievesGuild( uint32_t monsterCount, int guildCount )
599 {
600     assert( guildCount > 0 );
601     return guildCount == 1 ? "???" : Army::SizeString( monsterCount );
602 }
603 
PlayPickupSound(void)604 void Game::PlayPickupSound( void )
605 {
606     int wav = M82::UNKNOWN;
607 
608     switch ( Rand::Get( 1, 7 ) ) {
609     case 1:
610         wav = M82::PICKUP01;
611         break;
612     case 2:
613         wav = M82::PICKUP02;
614         break;
615     case 3:
616         wav = M82::PICKUP03;
617         break;
618     case 4:
619         wav = M82::PICKUP04;
620         break;
621     case 5:
622         wav = M82::PICKUP05;
623         break;
624     case 6:
625         wav = M82::PICKUP06;
626         break;
627     case 7:
628         wav = M82::PICKUP07;
629         break;
630 
631     default:
632         return;
633     }
634 
635     AGG::PlaySound( wav );
636 }
637