1 /***************************************************************************
2  *   Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2  *
3  *   Copyright (C) 2020                                                    *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
19  ***************************************************************************/
20 
21 #include <cassert>
22 
23 #include "agg.h"
24 #include "agg_image.h"
25 #include "battle.h"
26 #include "campaign_data.h"
27 #include "campaign_savedata.h"
28 #include "campaign_scenariodata.h"
29 #include "cursor.h"
30 #include "dialog.h"
31 #include "game.h"
32 #include "game_credits.h"
33 #include "game_io.h"
34 #include "game_video.h"
35 #include "icn.h"
36 #include "race.h"
37 #include "settings.h"
38 #include "text.h"
39 #include "translations.h"
40 #include "world.h"
41 
42 namespace
43 {
getCampaignIconOffsets(const int campaignId)44     std::vector<fheroes2::Point> getCampaignIconOffsets( const int campaignId )
45     {
46         switch ( campaignId ) {
47         case Campaign::ROLAND_CAMPAIGN:
48             return { { 0, 1 }, { 2, 1 }, { 3, 0 }, { 4, 1 }, { 6, 1 }, { 8, 1 }, { 10, 2 }, { 10, 0 }, { 12, 1 }, { 14, 1 } };
49         case Campaign::ARCHIBALD_CAMPAIGN:
50             return { { 0, 1 }, { 2, 1 }, { 4, 0 }, { 4, 2 }, { 6, 1 }, { 8, 1 }, { 9, 0 }, { 10, 1 }, { 12, 0 }, { 12, 2 }, { 14, 1 } };
51         case Campaign::PRICE_OF_LOYALTY_CAMPAIGN:
52             return { { 0, 0 }, { 2, 0 }, { 4, 1 }, { 4, 0 }, { 6, 1 }, { 7, 0 }, { 9, 1 }, { 10, 0 } };
53         case Campaign::DESCENDANTS_CAMPAIGN:
54             return { { 0, 1 }, { 2, 1 }, { 4, 0 }, { 4, 2 }, { 6, 1 }, { 8, 2 }, { 8, 0 }, { 10, 1 } };
55         case Campaign::WIZARDS_ISLE_CAMPAIGN:
56             return { { 0, 0 }, { 2, 0 }, { 4, 1 }, { 6, 0 } };
57         case Campaign::VOYAGE_HOME_CAMPAIGN:
58             return { { 0, 0 }, { 2, 0 }, { 4, 0 }, { 4, 1 } };
59         default:
60             // Implementing a new campaign? Add a new case!
61             assert( 0 );
62             return {};
63         }
64     }
65 
66     enum ScenarioIcon : uint32_t
67     {
68         SCENARIO_ICON_CLEARED = 0,
69         SCENARIO_ICON_AVAILABLE = 1,
70         SCENARIO_ICON_UNAVAILABLE = 2,
71     };
72 
DrawCampaignScenarioIcon(const int icnId,const int iconIdx,const fheroes2::Point & offset,const int posX,const int posY)73     void DrawCampaignScenarioIcon( const int icnId, const int iconIdx, const fheroes2::Point & offset, const int posX, const int posY )
74     {
75         const fheroes2::Sprite & icon = fheroes2::AGG::GetICN( icnId, iconIdx );
76         fheroes2::Blit( icon, fheroes2::Display::instance(), offset.x + posX, offset.y + posY );
77     }
78 
DrawCampaignScenarioIcons(fheroes2::ButtonGroup & buttonGroup,const Campaign::CampaignData & campaignData,const fheroes2::Point & top,const int chosenScenarioId)79     void DrawCampaignScenarioIcons( fheroes2::ButtonGroup & buttonGroup, const Campaign::CampaignData & campaignData, const fheroes2::Point & top,
80                                     const int chosenScenarioId )
81     {
82         fheroes2::Display & display = fheroes2::Display::instance();
83 
84         int campaignTrack = ICN::UNKNOWN;
85         int iconsId = ICN::UNKNOWN;
86         uint32_t iconStatusOffset = 0;
87         uint32_t selectedIconIdx = 0;
88 
89         switch ( campaignData.getCampaignID() ) {
90         case Campaign::ROLAND_CAMPAIGN:
91             campaignTrack = ICN::CTRACK00;
92             iconsId = ICN::CAMPXTRG;
93             iconStatusOffset = 10;
94             selectedIconIdx = 14;
95             break;
96         case Campaign::ARCHIBALD_CAMPAIGN:
97             campaignTrack = ICN::CTRACK03;
98             iconsId = ICN::CAMPXTRE;
99             iconStatusOffset = 10;
100             selectedIconIdx = 17;
101             break;
102         case Campaign::PRICE_OF_LOYALTY_CAMPAIGN:
103             campaignTrack = ICN::X_TRACK1;
104             iconsId = ICN::X_CMPEXT;
105             iconStatusOffset = 0;
106             selectedIconIdx = 4;
107             break;
108         case Campaign::DESCENDANTS_CAMPAIGN:
109             campaignTrack = ICN::X_TRACK2;
110             iconsId = ICN::X_CMPEXT;
111             iconStatusOffset = 0;
112             selectedIconIdx = 7;
113             break;
114         case Campaign::WIZARDS_ISLE_CAMPAIGN:
115             campaignTrack = ICN::X_TRACK3;
116             iconsId = ICN::X_CMPEXT;
117             iconStatusOffset = 0;
118             selectedIconIdx = 10;
119             break;
120         case Campaign::VOYAGE_HOME_CAMPAIGN:
121             campaignTrack = ICN::X_TRACK4;
122             iconsId = ICN::X_CMPEXT;
123             iconStatusOffset = 0;
124             selectedIconIdx = 13;
125             break;
126         default:
127             // Implementing a new campaign? Add a new case!
128             assert( 0 );
129             break;
130         }
131 
132         const fheroes2::Sprite & track = fheroes2::AGG::GetICN( campaignTrack, 0 );
133         const fheroes2::Point trackOffset( top.x + track.x(), top.y + track.y() );
134         fheroes2::Blit( track, display, trackOffset.x, trackOffset.y );
135 
136         const std::vector<fheroes2::Point> & iconOffsets = getCampaignIconOffsets( campaignData.getCampaignID() );
137         const int deltaY = 42;
138         const int deltaX = 37;
139 
140         const std::vector<Campaign::ScenarioData> & scenarios = campaignData.getAllScenarios();
141         const Campaign::CampaignSaveData & saveData = Campaign::CampaignSaveData::Get();
142 
143         std::vector<int> prevScenarioNextMaps;
144         const std::vector<int> & clearedMaps = saveData.getFinishedMaps();
145         std::vector<int> availableMaps;
146         if ( chosenScenarioId >= 0 ) {
147             availableMaps.emplace_back( chosenScenarioId );
148         }
149         else {
150             availableMaps = saveData.isStarting() ? campaignData.getStartingScenarios() : campaignData.getScenariosAfter( saveData.getLastCompletedScenarioID() );
151         }
152 
153         assert( iconOffsets.size() == scenarios.size() );
154 
155         for ( size_t i = 0; i < scenarios.size(); ++i ) {
156             const int scenarioID = scenarios[i].getScenarioID();
157 
158             assert( scenarioID >= 0 && static_cast<size_t>( scenarioID ) < iconOffsets.size() );
159             if ( scenarioID < 0 || static_cast<size_t>( scenarioID ) >= iconOffsets.size() ) {
160                 continue;
161             }
162 
163             fheroes2::Point offset = iconOffsets[scenarioID];
164             offset.x *= deltaX;
165             offset.y *= deltaY;
166 
167             offset.x -= 2;
168             offset.y -= 2;
169 
170             // available scenario (one of which should be selected)
171             if ( std::find( availableMaps.begin(), availableMaps.end(), scenarioID ) != availableMaps.end() ) {
172                 const fheroes2::Sprite & availableIcon = fheroes2::AGG::GetICN( iconsId, iconStatusOffset + SCENARIO_ICON_AVAILABLE );
173                 const fheroes2::Sprite & selectedIcon = fheroes2::AGG::GetICN( iconsId, selectedIconIdx );
174                 buttonGroup.createButton( trackOffset.x + offset.x, trackOffset.y + offset.y, availableIcon, selectedIcon, static_cast<int>( i ) );
175             }
176             // cleared scenario
177             else if ( std::find( clearedMaps.begin(), clearedMaps.end(), static_cast<int>( i ) ) != clearedMaps.end() ) {
178                 DrawCampaignScenarioIcon( iconsId, iconStatusOffset + SCENARIO_ICON_CLEARED, trackOffset, offset.x, offset.y );
179             }
180             else {
181                 DrawCampaignScenarioIcon( iconsId, iconStatusOffset + SCENARIO_ICON_UNAVAILABLE, trackOffset, offset.x, offset.y );
182             }
183         }
184     }
185 
DrawCampaignScenarioDescription(const Campaign::ScenarioData & scenario,const fheroes2::Point & top)186     void DrawCampaignScenarioDescription( const Campaign::ScenarioData & scenario, const fheroes2::Point & top )
187     {
188         const std::vector<Campaign::ScenarioBonusData> & bonuses = scenario.getBonuses();
189         TextBox mapName( scenario.getScenarioName(), Font::BIG, 200 );
190         mapName.Blit( top.x + 197, top.y + 97 - mapName.h() / 2 );
191 
192         Text campaignMapId( std::to_string( scenario.getScenarioID() + 1 ), Font::BIG );
193         campaignMapId.Blit( top.x + 172 - campaignMapId.w() / 2, top.y + 97 - campaignMapId.h() / 2 );
194 
195         TextBox mapDescription( scenario.getDescription(), Font::BIG, 356 );
196         mapDescription.Blit( top.x + 34, top.y + 132 );
197 
198         const int textChoiceWidth = 150;
199         for ( size_t i = 0; i < bonuses.size(); ++i ) {
200             Text choice( bonuses[i].ToString(), Font::BIG );
201             choice.Blit( top.x + 425, top.y + 209 + 22 * static_cast<int>( i ) - choice.h() / 2, textChoiceWidth );
202         }
203     }
204 
drawObtainedCampaignAwards(const std::vector<Campaign::CampaignAwardData> & obtainedAwards,const fheroes2::Point & top)205     void drawObtainedCampaignAwards( const std::vector<Campaign::CampaignAwardData> & obtainedAwards, const fheroes2::Point & top )
206     {
207         const int textAwardWidth = 180;
208 
209         // if there are more than 3 awards, we need to reduce the offset between text so that it doesn't overflow out of the text box
210         const size_t awardCount = obtainedAwards.size();
211         const size_t indexEnd = awardCount <= 4 ? awardCount : 4;
212         const int yOffset = awardCount > 3 ? 16 : 22;
213 
214         Text award;
215         for ( size_t i = 0; i < indexEnd; ++i ) {
216             if ( i < 3 )
217                 award.Set( obtainedAwards[i].ToString(), Font::BIG );
218             else // if we have exactly 4 obtained awards, display the fourth award, otherwise show "and more..."
219                 award.Set( awardCount == 4 ? obtainedAwards[i].ToString() : std::string( _( "and more..." ) ), Font::BIG );
220 
221             if ( award.w() > textAwardWidth ) {
222                 award.Blit( top.x + 425, top.y + 100 + yOffset * static_cast<int>( i ) - award.h() / 2, textAwardWidth );
223             }
224             else {
225                 award.Blit( top.x + 425 + ( textAwardWidth - award.w() ) / 2, top.y + 100 + yOffset * static_cast<int>( i ) - award.h() / 2 );
226             }
227         }
228     }
229 
replaceArmy(Army & army,const std::vector<Troop> & troops)230     void replaceArmy( Army & army, const std::vector<Troop> & troops )
231     {
232         army.Clean();
233         for ( size_t i = 0; i < troops.size(); ++i )
234             army.GetTroop( i )->Set( troops[i] );
235     }
236 
setHeroAndArmyBonus(Heroes * hero,const int campaignID,const uint32_t currentScenarioID)237     void setHeroAndArmyBonus( Heroes * hero, const int campaignID, const uint32_t currentScenarioID )
238     {
239         switch ( campaignID ) {
240         case Campaign::ARCHIBALD_CAMPAIGN: {
241             if ( currentScenarioID != 6 ) {
242                 assert( 0 ); // no other scenario has this bonus
243                 return;
244             }
245             switch ( hero->GetRace() ) {
246             case Race::NECR:
247                 replaceArmy( hero->GetArmy(), { { Monster::SKELETON, 50 }, { Monster::ROYAL_MUMMY, 18 }, { Monster::VAMPIRE_LORD, 8 } } );
248                 break;
249             case Race::WRLK:
250                 replaceArmy( hero->GetArmy(), { { Monster::CENTAUR, 40 }, { Monster::GARGOYLE, 24 }, { Monster::GRIFFIN, 18 } } );
251                 break;
252             case Race::BARB:
253                 replaceArmy( hero->GetArmy(), { { Monster::ORC_CHIEF, 12 }, { Monster::OGRE, 18 }, { Monster::GOBLIN, 40 } } );
254                 break;
255             default:
256                 assert( 0 ); // bonus changed?
257             }
258             const uint32_t exp = hero->GetExperience();
259             if ( exp < 5000 ) {
260                 hero->IncreaseExperience( 5000 - exp, true );
261             }
262             break;
263         }
264         default:
265             assert( 0 ); // some new campaign that uses this bonus?
266         }
267     }
268 
SetScenarioBonus(const int campaignID,const uint32_t currentScenarioID,const Campaign::ScenarioBonusData & scenarioBonus)269     void SetScenarioBonus( const int campaignID, const uint32_t currentScenarioID, const Campaign::ScenarioBonusData & scenarioBonus )
270     {
271         const Players & sortedPlayers = Settings::Get().GetPlayers();
272         for ( const Player * player : sortedPlayers ) {
273             if ( player == nullptr ) {
274                 continue;
275             }
276 
277             if ( !player->isControlHuman() )
278                 continue;
279 
280             Kingdom & kingdom = world.GetKingdom( player->GetColor() );
281             Heroes * bestHero = kingdom.GetBestHero();
282 
283             switch ( scenarioBonus._type ) {
284             case Campaign::ScenarioBonusData::RESOURCES:
285                 kingdom.AddFundsResource( Funds( scenarioBonus._subType, scenarioBonus._amount ) );
286                 break;
287             case Campaign::ScenarioBonusData::ARTIFACT: {
288                 assert( bestHero != nullptr );
289                 if ( bestHero != nullptr ) {
290                     bestHero->PickupArtifact( Artifact( scenarioBonus._subType ) );
291                 }
292                 break;
293             }
294             case Campaign::ScenarioBonusData::TROOP:
295                 assert( bestHero != nullptr );
296                 if ( bestHero != nullptr ) {
297                     bestHero->GetArmy().JoinTroop( Troop( Monster( scenarioBonus._subType ), scenarioBonus._amount ) );
298                 }
299                 break;
300             case Campaign::ScenarioBonusData::SPELL: {
301                 KingdomHeroes & heroes = kingdom.GetHeroes();
302                 assert( !heroes.empty() );
303                 if ( !heroes.empty() ) {
304                     // TODO: make sure that the correct hero receives the spell. Right now it's a semi-hacky way to do this.
305                     heroes.back()->AppendSpellToBook( scenarioBonus._subType, true );
306                 }
307                 break;
308             }
309             case Campaign::ScenarioBonusData::STARTING_RACE:
310                 Players::SetPlayerRace( player->GetColor(), scenarioBonus._subType );
311                 break;
312             case Campaign::ScenarioBonusData::STARTING_RACE_AND_ARMY:
313                 assert( bestHero != nullptr );
314                 if ( bestHero != nullptr ) {
315                     setHeroAndArmyBonus( bestHero, campaignID, currentScenarioID );
316                 }
317                 break;
318             case Campaign::ScenarioBonusData::SKILL_PRIMARY:
319                 assert( bestHero != nullptr );
320                 if ( bestHero != nullptr ) {
321                     for ( uint32_t i = 0; i < scenarioBonus._amount; ++i )
322                         bestHero->IncreasePrimarySkill( scenarioBonus._subType );
323                 }
324                 break;
325             case Campaign::ScenarioBonusData::SKILL_SECONDARY:
326                 assert( bestHero != nullptr );
327                 if ( bestHero != nullptr ) {
328                     bestHero->LearnSkill( Skill::Secondary( scenarioBonus._subType, scenarioBonus._amount ) );
329                 }
330                 break;
331             default:
332                 assert( 0 );
333             }
334         }
335     }
336 
337     // apply only the ones that are applied at the start (artifact, spell, carry-over troops)
338     // the rest will be applied based on the situation required
applyObtainedCampaignAwards(const uint32_t currentScenarioID,const std::vector<Campaign::CampaignAwardData> & awards)339     void applyObtainedCampaignAwards( const uint32_t currentScenarioID, const std::vector<Campaign::CampaignAwardData> & awards )
340     {
341         const Players & sortedPlayers = Settings::Get().GetPlayers();
342         Kingdom & humanKingdom = world.GetKingdom( Players::HumanColors() );
343 
344         for ( size_t i = 0; i < awards.size(); ++i ) {
345             if ( currentScenarioID < awards[i]._startScenarioID )
346                 continue;
347 
348             switch ( awards[i]._type ) {
349             case Campaign::CampaignAwardData::TYPE_GET_ARTIFACT:
350                 humanKingdom.GetBestHero()->PickupArtifact( Artifact( awards[i]._subType ) );
351                 break;
352             case Campaign::CampaignAwardData::TYPE_GET_SPELL:
353                 humanKingdom.GetBestHero()->AppendSpellToBook( awards[i]._subType, true );
354                 break;
355             case Campaign::CampaignAwardData::TYPE_DEFEAT_ENEMY_HERO:
356                 for ( const Player * player : sortedPlayers ) {
357                     Kingdom & kingdom = world.GetKingdom( player->GetColor() );
358                     const KingdomHeroes & heroes = kingdom.GetHeroes();
359 
360                     for ( size_t j = 0; j < heroes.size(); ++j ) {
361                         if ( heroes[j]->GetID() == static_cast<int>( awards[i]._subType ) ) {
362                             heroes[j]->SetKillerColor( humanKingdom.GetColor() );
363                             heroes[j]->SetFreeman( Battle::RESULT_LOSS );
364                             break;
365                         }
366                     }
367                 }
368                 break;
369             case Campaign::CampaignAwardData::TYPE_CARRY_OVER_FORCES:
370                 replaceArmy( humanKingdom.GetBestHero()->GetArmy(), Campaign::CampaignSaveData::Get().getCarryOverTroops() );
371                 break;
372             }
373         }
374     }
375 
playPreviosScenarioVideo()376     void playPreviosScenarioVideo()
377     {
378         const Campaign::CampaignSaveData & saveData = Campaign::CampaignSaveData::Get();
379         if ( saveData.isStarting() ) {
380             return;
381         }
382 
383         const int lastCompletedScenarioID = saveData.getLastCompletedScenarioID();
384         const Campaign::CampaignData & campaignData = Campaign::CampaignData::getCampaignData( saveData.getCampaignID() );
385 
386         const std::vector<Campaign::ScenarioData> & scenarios = campaignData.getAllScenarios();
387         assert( lastCompletedScenarioID >= 0 && static_cast<size_t>( lastCompletedScenarioID ) < scenarios.size() );
388         const Campaign::ScenarioData & completedScenario = scenarios[lastCompletedScenarioID];
389 
390         if ( !completedScenario.getEndScenarioVideoPlayback().empty() ) {
391             AGG::ResetMixer();
392 
393             for ( const Campaign::ScenarioIntroVideoInfo & videoInfo : completedScenario.getEndScenarioVideoPlayback() ) {
394                 Video::ShowVideo( videoInfo.fileName, videoInfo.action );
395             }
396 
397             AGG::ResetMixer();
398         }
399     }
400 
playCurrentScenarioVideo()401     void playCurrentScenarioVideo()
402     {
403         const Campaign::CampaignSaveData & saveData = Campaign::CampaignSaveData::Get();
404 
405         const int chosenScenarioID = saveData.getCurrentScenarioID();
406         const Campaign::CampaignData & campaignData = Campaign::CampaignData::getCampaignData( saveData.getCampaignID() );
407 
408         const std::vector<Campaign::ScenarioData> & scenarios = campaignData.getAllScenarios();
409         assert( chosenScenarioID >= 0 && static_cast<size_t>( chosenScenarioID ) < scenarios.size() );
410         const Campaign::ScenarioData & scenario = scenarios[chosenScenarioID];
411 
412         if ( !scenario.getStartScenarioVideoPlayback().empty() ) {
413             AGG::ResetMixer();
414 
415             for ( const Campaign::ScenarioIntroVideoInfo & videoInfo : scenario.getStartScenarioVideoPlayback() ) {
416                 Video::ShowVideo( videoInfo.fileName, videoInfo.action );
417             }
418 
419             AGG::ResetMixer();
420         }
421     }
422 
getCampaignButtonId(const int campaignId)423     int getCampaignButtonId( const int campaignId )
424     {
425         switch ( campaignId ) {
426         case Campaign::ROLAND_CAMPAIGN:
427             return ICN::CAMPXTRG;
428         case Campaign::ARCHIBALD_CAMPAIGN:
429             return ICN::CAMPXTRE;
430         case Campaign::PRICE_OF_LOYALTY_CAMPAIGN:
431         case Campaign::DESCENDANTS_CAMPAIGN:
432         case Campaign::WIZARDS_ISLE_CAMPAIGN:
433         case Campaign::VOYAGE_HOME_CAMPAIGN:
434             return ICN::X_CMPBTN;
435         default:
436             // Implementing a new campaign? Add a new case!
437             assert( 0 );
438             return ICN::UNKNOWN;
439         }
440     }
441 
drawCampaignNameHeader(const int campaignId,fheroes2::Image & output,const fheroes2::Point & offset)442     void drawCampaignNameHeader( const int campaignId, fheroes2::Image & output, const fheroes2::Point & offset )
443     {
444         // Add extra image header if supported
445         uint32_t campaignNameHeader = ICN::UNKNOWN;
446 
447         switch ( campaignId ) {
448         case Campaign::PRICE_OF_LOYALTY_CAMPAIGN:
449             campaignNameHeader = 15;
450             break;
451         case Campaign::DESCENDANTS_CAMPAIGN:
452             campaignNameHeader = 16;
453             break;
454         case Campaign::WIZARDS_ISLE_CAMPAIGN:
455             campaignNameHeader = 17;
456             break;
457         case Campaign::VOYAGE_HOME_CAMPAIGN:
458             campaignNameHeader = 18;
459             break;
460         default:
461             return;
462         }
463 
464         const fheroes2::Sprite & header = fheroes2::AGG::GetICN( ICN::X_CMPEXT, campaignNameHeader );
465         fheroes2::Blit( header, output, offset.x + 24, offset.y + 25 );
466     }
467 
playCampaignMusic(const int campaignId)468     void playCampaignMusic( const int campaignId )
469     {
470         switch ( campaignId ) {
471         case Campaign::ROLAND_CAMPAIGN:
472         case Campaign::PRICE_OF_LOYALTY_CAMPAIGN:
473         case Campaign::DESCENDANTS_CAMPAIGN:
474         case Campaign::WIZARDS_ISLE_CAMPAIGN:
475         case Campaign::VOYAGE_HOME_CAMPAIGN:
476             AGG::PlayMusic( MUS::ROLAND_CAMPAIGN_SCREEN, true );
477             break;
478         case Campaign::ARCHIBALD_CAMPAIGN:
479             AGG::PlayMusic( MUS::ARCHIBALD_CAMPAIGN_SCREEN, true );
480             break;
481         default:
482             // Implementing a new campaign? Add a new case!
483             assert( 0 );
484             break;
485         }
486     }
487 }
488 
isSuccessionWarsCampaignPresent()489 bool Game::isSuccessionWarsCampaignPresent()
490 {
491     return Campaign::CampaignData::getCampaignData( Campaign::ROLAND_CAMPAIGN ).isAllCampaignMapsPresent()
492            && Campaign::CampaignData::getCampaignData( Campaign::ARCHIBALD_CAMPAIGN ).isAllCampaignMapsPresent();
493 }
494 
isPriceOfLoyaltyCampaignPresent()495 bool Game::isPriceOfLoyaltyCampaignPresent()
496 {
497     // We need to check game resources as well.
498     if ( fheroes2::AGG::GetICN( ICN::X_LOADCM, 0 ).empty() || fheroes2::AGG::GetICN( ICN::X_IVY, 0 ).empty() ) {
499         return false;
500     }
501 
502     return Campaign::CampaignData::getCampaignData( Campaign::PRICE_OF_LOYALTY_CAMPAIGN ).isAllCampaignMapsPresent()
503            && Campaign::CampaignData::getCampaignData( Campaign::VOYAGE_HOME_CAMPAIGN ).isAllCampaignMapsPresent()
504            && Campaign::CampaignData::getCampaignData( Campaign::WIZARDS_ISLE_CAMPAIGN ).isAllCampaignMapsPresent()
505            && Campaign::CampaignData::getCampaignData( Campaign::DESCENDANTS_CAMPAIGN ).isAllCampaignMapsPresent();
506 }
507 
CompleteCampaignScenario(const bool isLoadingSaveFile)508 fheroes2::GameMode Game::CompleteCampaignScenario( const bool isLoadingSaveFile )
509 {
510     Campaign::CampaignSaveData & saveData = Campaign::CampaignSaveData::Get();
511 
512     if ( !isLoadingSaveFile ) {
513         saveData.addCurrentMapToFinished();
514         saveData.addDaysPassed( world.CountDay() );
515         Game::SaveCompletedCampaignScenario();
516     }
517 
518     const int lastCompletedScenarioID = saveData.getLastCompletedScenarioID();
519     const Campaign::CampaignData & campaignData = Campaign::CampaignData::getCampaignData( saveData.getCampaignID() );
520 
521     const std::vector<Campaign::CampaignAwardData> obtainableAwards
522         = Campaign::CampaignAwardData::getCampaignAwardData( saveData.getCampaignID(), lastCompletedScenarioID );
523 
524     // TODO: Check for awards that have to be obtained with 'freak' conditions
525     for ( size_t i = 0; i < obtainableAwards.size(); ++i ) {
526         const uint32_t awardType = obtainableAwards[i]._type;
527 
528         if ( awardType == Campaign::CampaignAwardData::AwardType::TYPE_CARRY_OVER_FORCES ) {
529             Kingdom & humanKingdom = world.GetKingdom( Players::HumanColors() );
530 
531             const Heroes * lastBattleWinHero = humanKingdom.GetLastBattleWinHero();
532 
533             if ( lastBattleWinHero )
534                 saveData.setCarryOverTroops( lastBattleWinHero->GetArmy() );
535         }
536 
537         saveData.addCampaignAward( obtainableAwards[i]._id );
538 
539         // after adding an artifact award, check whether the artifacts can be assembled into something else
540         if ( awardType == Campaign::CampaignAwardData::AwardType::TYPE_GET_ARTIFACT ) {
541             const std::vector<Campaign::CampaignAwardData> obtainedAwards = saveData.getObtainedCampaignAwards();
542             std::map<uint32_t, int> artifactAwardIDs;
543             BagArtifacts bagArtifacts;
544 
545             for ( const Campaign::CampaignAwardData & awardData : obtainedAwards ) {
546                 if ( awardData._type != Campaign::CampaignAwardData::AwardType::TYPE_GET_ARTIFACT )
547                     continue;
548 
549                 artifactAwardIDs.emplace( awardData._subType, awardData._id );
550                 bagArtifacts.PushArtifact( awardData._subType );
551                 saveData.removeCampaignAward( awardData._id );
552             }
553 
554             // add the assembled artifact's campaign award to artifactAwards
555             for ( const Campaign::CampaignAwardData & awardData : Campaign::CampaignAwardData::getExtraCampaignAwardData( saveData.getCampaignID() ) ) {
556                 if ( awardData._type != Campaign::CampaignAwardData::AwardType::TYPE_GET_ARTIFACT )
557                     continue;
558 
559                 artifactAwardIDs.emplace( awardData._subType, awardData._id );
560             }
561 
562             bagArtifacts.assembleArtifactSetIfPossible();
563 
564             for ( const Artifact & artifact : bagArtifacts ) {
565                 if ( !artifact.isValid() )
566                     continue;
567 
568                 const auto foundArtifact = artifactAwardIDs.find( artifact.GetID() );
569                 if ( foundArtifact != artifactAwardIDs.end() )
570                     saveData.addCampaignAward( foundArtifact->second );
571             }
572         }
573     }
574 
575     playPreviosScenarioVideo();
576 
577     // TODO: do proper calc based on all scenarios cleared?
578     if ( campaignData.isLastScenario( lastCompletedScenarioID ) ) {
579         Game::ShowCredits();
580 
581         AGG::ResetMixer();
582         Video::ShowVideo( "WIN.SMK", Video::VideoAction::WAIT_FOR_USER_INPUT );
583         return fheroes2::GameMode::HIGHSCORES;
584     }
585 
586     const int firstNextMap = campaignData.getScenariosAfter( lastCompletedScenarioID ).front();
587     saveData.setCurrentScenarioID( firstNextMap );
588     return fheroes2::GameMode::SELECT_CAMPAIGN_SCENARIO;
589 }
590 
SelectCampaignScenario(const fheroes2::GameMode prevMode,const bool allowToRestart)591 fheroes2::GameMode Game::SelectCampaignScenario( const fheroes2::GameMode prevMode, const bool allowToRestart )
592 {
593     fheroes2::Display & display = fheroes2::Display::instance();
594     display.fill( 0 );
595     Settings & conf = Settings::Get();
596 
597     // setup cursor
598     const CursorRestorer cursorRestorer( true, Cursor::POINTER );
599 
600     Campaign::CampaignSaveData & campaignSaveData = Campaign::CampaignSaveData::Get();
601     const int chosenCampaignID = campaignSaveData.getCampaignID();
602 
603     const Campaign::CampaignData & campaignData = Campaign::CampaignData::getCampaignData( chosenCampaignID );
604 
605     const int chosenScenarioID = campaignSaveData.getCurrentScenarioID();
606     const std::vector<Campaign::ScenarioData> & scenarios = campaignData.getAllScenarios();
607     const Campaign::ScenarioData & scenario = scenarios[chosenScenarioID];
608 
609     if ( !allowToRestart ) {
610         playCurrentScenarioVideo();
611     }
612 
613     playCampaignMusic( chosenCampaignID );
614 
615     int backgroundIconID = ICN::UNKNOWN;
616 
617     switch ( chosenCampaignID ) {
618     case Campaign::ROLAND_CAMPAIGN:
619         backgroundIconID = ICN::CAMPBKGG;
620         break;
621     case Campaign::ARCHIBALD_CAMPAIGN:
622         backgroundIconID = ICN::CAMPBKGE;
623         break;
624         // PoL campaigns use the same background, but different headers. TODO: Implement the headers
625     case Campaign::PRICE_OF_LOYALTY_CAMPAIGN:
626     case Campaign::DESCENDANTS_CAMPAIGN:
627     case Campaign::WIZARDS_ISLE_CAMPAIGN:
628     case Campaign::VOYAGE_HOME_CAMPAIGN:
629         backgroundIconID = ICN::X_CMPBKG;
630         break;
631     default:
632         // Implementing a new campaign? Add a new case!
633         assert( 0 );
634         break;
635     }
636 
637     const fheroes2::Sprite & backgroundImage = fheroes2::AGG::GetICN( backgroundIconID, 0 );
638     const fheroes2::Point top( ( display.width() - backgroundImage.width() ) / 2, ( display.height() - backgroundImage.height() ) / 2 );
639 
640     fheroes2::Blit( backgroundImage, display, top.x, top.y );
641     drawCampaignNameHeader( chosenCampaignID, display, top );
642 
643     const int buttonIconID = getCampaignButtonId( chosenCampaignID );
644     fheroes2::Button buttonViewIntro( top.x + 22, top.y + 431, buttonIconID, 0, 1 );
645     fheroes2::Button buttonRestart( top.x + 195, top.y + 431, buttonIconID, 2, 3 );
646     fheroes2::Button buttonOk( top.x + 367, top.y + 431, buttonIconID, 4, 5 );
647     fheroes2::Button buttonCancel( top.x + 511, top.y + 431, buttonIconID, 6, 7 );
648 
649     // create scenario bonus choice buttons
650     fheroes2::ButtonGroup buttonChoices;
651     fheroes2::OptionButtonGroup optionButtonGroup;
652 
653     Campaign::ScenarioBonusData scenarioBonus;
654     const std::vector<Campaign::ScenarioBonusData> & bonusChoices = scenario.getBonuses();
655 
656     const fheroes2::Point optionButtonOffset( 590, 199 );
657     const int32_t optionButtonStep = 22;
658 
659     const fheroes2::Sprite & pressedButton = fheroes2::AGG::GetICN( ICN::CAMPXTRG, allowToRestart ? 9 : 8 );
660     fheroes2::Sprite releaseButton( pressedButton.width(), pressedButton.height(), pressedButton.x(), pressedButton.y() );
661     fheroes2::Copy( backgroundImage, optionButtonOffset.x + pressedButton.x(), optionButtonOffset.y + pressedButton.y(), releaseButton, 0, 0, releaseButton.width(),
662                     releaseButton.height() );
663 
664     const uint32_t bonusChoiceCount = static_cast<uint32_t>( scenario.getBonuses().size() );
665     for ( uint32_t i = 0; i < bonusChoiceCount; ++i ) {
666         buttonChoices.createButton( optionButtonOffset.x + top.x, optionButtonOffset.y + optionButtonStep * i + top.y, releaseButton, pressedButton, i );
667         optionButtonGroup.addButton( &buttonChoices.button( i ) );
668     }
669 
670     // in case there's no bonus for the map
671     if ( bonusChoiceCount > 0 ) {
672         scenarioBonus = bonusChoices[0];
673         buttonChoices.button( 0 ).press();
674     }
675 
676     buttonViewIntro.draw();
677 
678     if ( !scenario.isMapFilePresent() ) {
679         buttonOk.disable();
680     }
681 
682     if ( allowToRestart ) {
683         buttonOk.disable();
684         buttonOk.hide();
685         buttonRestart.draw();
686     }
687     else {
688         buttonRestart.disable();
689         buttonRestart.hide();
690         buttonOk.draw();
691     }
692 
693     buttonCancel.draw();
694 
695     for ( uint32_t i = 0; i < bonusChoiceCount; ++i )
696         buttonChoices.button( i ).draw();
697 
698     Text textDaysSpent( std::to_string( campaignSaveData.getDaysPassed() ), Font::BIG );
699     textDaysSpent.Blit( top.x + 582 - textDaysSpent.w() / 2, top.y + 31 );
700 
701     DrawCampaignScenarioDescription( scenario, top );
702     drawObtainedCampaignAwards( campaignSaveData.getObtainedCampaignAwards(), top );
703 
704     std::vector<int> selectableScenarios;
705     if ( allowToRestart ) {
706         selectableScenarios.emplace_back( chosenScenarioID );
707     }
708     else {
709         selectableScenarios
710             = campaignSaveData.isStarting() ? campaignData.getStartingScenarios() : campaignData.getScenariosAfter( campaignSaveData.getLastCompletedScenarioID() );
711     }
712 
713     const uint32_t selectableScenariosCount = static_cast<uint32_t>( selectableScenarios.size() );
714 
715     fheroes2::ButtonGroup selectableScenarioButtons;
716 
717     const int highlightedScenarioId = allowToRestart ? chosenScenarioID : -1;
718     DrawCampaignScenarioIcons( selectableScenarioButtons, campaignData, top, highlightedScenarioId );
719 
720     for ( uint32_t i = 0; i < selectableScenariosCount; ++i ) {
721         if ( chosenScenarioID == selectableScenarios[i] )
722             selectableScenarioButtons.button( i ).press();
723 
724         selectableScenarioButtons.button( i ).draw();
725     }
726 
727     LocalEvent & le = LocalEvent::Get();
728 
729     display.render();
730 
731     while ( le.HandleEvents() ) {
732         le.MousePressLeft( buttonCancel.area() ) ? buttonCancel.drawOnPress() : buttonCancel.drawOnRelease();
733         le.MousePressLeft( buttonOk.area() ) ? buttonOk.drawOnPress() : buttonOk.drawOnRelease();
734         le.MousePressLeft( buttonViewIntro.area() ) ? buttonViewIntro.drawOnPress() : buttonViewIntro.drawOnRelease();
735 
736         if ( allowToRestart ) {
737             le.MousePressLeft( buttonRestart.area() ) ? buttonRestart.drawOnPress() : buttonRestart.drawOnRelease();
738         }
739         else {
740             for ( uint32_t i = 0; i < bonusChoiceCount; ++i ) {
741                 if ( le.MousePressLeft( buttonChoices.button( i ).area() ) ) {
742                     buttonChoices.button( i ).press();
743                     optionButtonGroup.draw();
744                     scenarioBonus = bonusChoices[i];
745 
746                     break;
747                 }
748             }
749         }
750 
751         for ( uint32_t i = 0; i < selectableScenariosCount; ++i ) {
752             if ( chosenScenarioID != selectableScenarios[i] && le.MousePressLeft( selectableScenarioButtons.button( i ).area() ) ) {
753                 campaignSaveData.setCurrentScenarioID( selectableScenarios[i] );
754                 return fheroes2::GameMode::SELECT_CAMPAIGN_SCENARIO;
755             }
756         }
757 
758         if ( le.MouseClickLeft( buttonCancel.area() ) || HotKeyPressEvent( EVENT_DEFAULT_EXIT ) ) {
759             return prevMode;
760         }
761         if ( ( buttonOk.isEnabled() && ( le.MouseClickLeft( buttonOk.area() ) || HotKeyPressEvent( EVENT_DEFAULT_READY ) ) )
762              || ( buttonRestart.isEnabled() && le.MouseClickLeft( buttonRestart.area() ) ) ) {
763             const Maps::FileInfo mapInfo = scenario.loadMap();
764             conf.SetCurrentFileInfo( mapInfo );
765 
766             // starting faction scenario bonus has to be called before players.SetStartGame()
767             if ( scenarioBonus._type == Campaign::ScenarioBonusData::STARTING_RACE || scenarioBonus._type == Campaign::ScenarioBonusData::STARTING_RACE_AND_ARMY ) {
768                 // but the army has to be set after starting the game, so first only set the race
769                 SetScenarioBonus( campaignSaveData.getCampaignID(), chosenScenarioID,
770                                   { Campaign::ScenarioBonusData::STARTING_RACE, scenarioBonus._subType, scenarioBonus._amount } );
771             }
772 
773             Players & players = conf.GetPlayers();
774             players.SetStartGame();
775             if ( conf.ExtGameUseFade() )
776                 fheroes2::FadeDisplay();
777 
778             fheroes2::ImageRestorer restorer( display );
779             Game::ShowMapLoadingText();
780             conf.SetGameType( Game::TYPE_CAMPAIGN );
781 
782             if ( !world.LoadMapMP2( mapInfo.file ) ) {
783                 Dialog::Message( _( "Campaign Scenario loading failure" ), _( "Please make sure that campaign files are correct and present." ), Font::BIG, Dialog::OK );
784                 conf.SetCurrentFileInfo( Maps::FileInfo() );
785                 continue;
786             }
787 
788             restorer.reset();
789 
790             // meanwhile, the others should be called after players.SetStartGame()
791             if ( scenarioBonus._type != Campaign::ScenarioBonusData::STARTING_RACE ) {
792                 SetScenarioBonus( campaignSaveData.getCampaignID(), chosenScenarioID, scenarioBonus );
793             }
794 
795             applyObtainedCampaignAwards( chosenScenarioID, campaignSaveData.getObtainedCampaignAwards() );
796 
797             campaignSaveData.setCurrentScenarioBonus( scenarioBonus );
798             campaignSaveData.setCurrentScenarioID( chosenScenarioID );
799 
800             return fheroes2::GameMode::START_GAME;
801         }
802         else if ( le.MouseClickLeft( buttonViewIntro.area() ) ) {
803             AGG::ResetMixer();
804             fheroes2::ImageRestorer restorer( display, top.x, top.y, backgroundImage.width(), backgroundImage.height() );
805             playPreviosScenarioVideo();
806             playCurrentScenarioVideo();
807 
808             playCampaignMusic( chosenCampaignID );
809         }
810     }
811 
812     return prevMode;
813 }
814