1 /*
2     SPDX-FileCopyrightText: 2009 Mathias Kraus <k.hias@gmx.de>
3     SPDX-FileCopyrightText: 2007-2008 Thomas Gallinari <tg8187@yahoo.fr>
4     SPDX-FileCopyrightText: 2007-2008 Pierre-Benoit Besse <besse@gmail.com>
5     SPDX-FileCopyrightText: 2007-2008 Alexandre Galinier <alex.galinier@hotmail.com>
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 
10 #include "game.h"
11 #include "mapparser.h"
12 #include "settings.h"
13 #include "gamescene.h"
14 #include "arena.h"
15 #include "player.h"
16 #include "bonus.h"
17 #include "bomb.h"
18 #include "block.h"
19 #include "config/playersettings.h"
20 #include "granatier_random.h"
21 
22 #include <QPointF>
23 #include <QTimer>
24 #include <QKeyEvent>
25 #include <QDir>
26 #include <KConfig>
27 #include <KgSound>
28 #include <QStandardPaths>
29 
30 
Game(PlayerSettings * playerSettings)31 Game::Game(PlayerSettings* playerSettings)
32 {
33     m_playerSettings = playerSettings;
34 
35     // Initialize the sound state
36     setSoundsEnabled(Settings::sounds());
37     m_wilhelmScream = Settings::useWilhelmScream();
38 
39     m_soundPutBomb = new KgSound(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/putbomb.wav")));
40     m_soundExplode = new KgSound(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/explode.wav")));
41     m_soundBonus = new KgSound(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/wow.wav")));
42     m_soundFalling = new KgSound(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/deepfall.wav")));
43     m_soundDie = new KgSound(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/die.wav")));
44 
45     m_arena = nullptr;
46     m_randomArenaModeArenaList.clear();
47     m_gameScene = nullptr;
48     m_winPoints = Settings::self()->pointsToWin();
49 
50     QStringList strPlayerIDs = m_playerSettings->playerIDs();
51     for(int i = 0; i < strPlayerIDs.count(); i++)
52     {
53         if(m_playerSettings->enabled(strPlayerIDs[i]))
54         {
55             Player* player = new Player(qreal(Granatier::CellSize * (-0.5)),qreal(Granatier::CellSize * 0.5), strPlayerIDs[i], playerSettings, m_arena);
56             m_players.append(player);
57             connect(player, &Player::dying, this, &Game::playerDeath);
58             connect(player, &Player::falling, this, &Game::playerFalling);
59             connect(player, &Player::resurrectBonusTaken, this, &Game::resurrectBonusTaken);
60         }
61     }
62 
63     init();
64 
65     for (auto & player : m_players)
66     {
67         connect(player, &Player::bombDropped, this, &Game::createBomb);
68     }
69 
70     m_gameOver = false;
71 }
72 
init()73 void Game::init()
74 {
75     // Create the Arena instance
76     m_arena = new Arena();
77 
78     m_remainingTime = Settings::roundTime();
79     m_bombCount = 0;
80 
81     // Create the parser that will parse the XML file in order to initialize the Arena instance
82     // This also creates all the characters
83     MapParser mapParser(m_arena);
84     // Set the XML file as input source for the parser
85     QString filePath;
86     if(Settings::self()->randomArenaMode())
87     {
88         if(m_randomArenaModeArenaList.isEmpty())
89         {
90             auto randomArenaModeArenaList = Settings::self()->randomArenaModeArenaList();
91 
92             QStringList arenasAvailable;
93             const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("arenas"), QStandardPaths::LocateDirectory);
94             for(const auto& dir: dirs) {
95                 const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.desktop"));
96                 for(const auto& file: fileNames) {
97                     arenasAvailable.append(dir + QLatin1Char('/') + file);
98                 }
99             }
100 
101             // store the random arenas if they are available
102             for(const auto& randomArena: randomArenaModeArenaList) {
103                 for(const auto& arena: arenasAvailable) {
104                     if(arena.endsWith(randomArena)) {
105                         m_randomArenaModeArenaList.append(arena);
106                         break;
107                     }
108                 }
109             }
110 
111             if(m_randomArenaModeArenaList.isEmpty()){
112                 m_randomArenaModeArenaList = arenasAvailable;
113             }
114         }
115 
116         int nIndex = granatier::RNG::fromRange(0, m_randomArenaModeArenaList.count() - 1);
117         filePath = m_randomArenaModeArenaList.at(nIndex);
118         m_randomArenaModeArenaList.removeAt(nIndex);
119     }
120     else
121     {
122         filePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, Settings::self()->arena());
123     }
124 
125     if(!QFile::exists(filePath))
126     {
127         Settings::self()->useDefaults(true);
128         filePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, Settings::self()->arena());
129         Settings::self()->useDefaults(false);
130     }
131 
132     KConfig arenaConfig(filePath, KConfig::SimpleConfig);
133     KConfigGroup group = arenaConfig.group("Arena");
134     QString arenaFileName = group.readEntry("FileName");
135 
136     QFile arenaXmlFile(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("arenas/%1").arg(arenaFileName)));
137     if (!arenaXmlFile.open(QIODevice::ReadOnly)) {
138         qWarning() << " impossible to open file " << arenaFileName;
139     }
140     mapParser.parse(&arenaXmlFile);
141 
142     QString arenaName = group.readEntry("Name");
143     m_arena->setName(arenaName);
144 
145     //create the block items
146     for (int i = 0; i < m_arena->getNbRows(); ++i)
147     {
148         for (int j = 0; j < m_arena->getNbColumns(); ++j)
149         {
150             if(m_arena->getCell(i,j).getType() == Granatier::Cell::BLOCK)
151             {
152                 Block* block = new Block((j + 0.5) * Granatier::CellSize, (i + 0.5) * Granatier::CellSize, m_arena, QStringLiteral("arena_block"));
153                 m_blocks.append(block);
154                 m_arena->setCellElement(i, j, block);
155             }
156         }
157     }
158 
159     // Start the Game timer
160     m_timer = new QTimer(this);
161     m_timer->setInterval(int(1000 / Granatier::FPS));
162     connect(m_timer, &QTimer::timeout, this, &Game::update);
163     m_timer->start();
164     m_state = RUNNING;
165 
166     m_roundTimer  = new QTimer(this);
167     m_roundTimer->setInterval(1000);
168     connect(m_roundTimer, &QTimer::timeout, this, &Game::decrementRemainingRoundTime);
169     m_roundTimer->start();
170 
171     m_setRoundFinishedTimer  = new QTimer(this);
172     m_setRoundFinishedTimer->setSingleShot(true);
173     connect(m_setRoundFinishedTimer, &QTimer::timeout, this, &Game::setRoundFinished);
174 
175     // Init the characters coordinates on the Arena
176     for (int i = 0; i < m_players.size(); i++)
177     {
178         m_players[i]->setArena(m_arena);
179         QPointF playerPosition = m_arena->getPlayerPosition(i);
180         m_players[i]->setInitialCoordinates(qreal(Granatier::CellSize * playerPosition.x()), qreal(Granatier::CellSize * playerPosition.y()));
181     }
182     initCharactersPosition();
183 
184     // Create the hidden Bonuses
185     createBonus();
186 }
187 
~Game()188 Game::~Game()
189 {
190     //pause is needed to stop all animations and therefore the access of the *items to their model
191     pause(true);
192 
193     qDeleteAll(m_players);
194     m_players.clear();
195 
196     cleanUp();
197 
198     delete m_soundPutBomb;
199     delete m_soundExplode;
200     delete m_soundBonus;
201     delete m_soundFalling;
202     delete m_soundDie;
203 }
204 
cleanUp()205 void Game::cleanUp()
206 {
207     qDeleteAll(m_blocks);
208     m_blocks.clear();
209     qDeleteAll(m_bonus);
210     m_bonus.clear();
211     qDeleteAll(m_bombs);
212     m_bombs.clear();
213     delete m_arena;
214     m_arena = nullptr;
215     delete m_timer;
216     m_timer = nullptr;
217     delete m_roundTimer;
218     m_roundTimer = nullptr;
219     delete m_setRoundFinishedTimer;
220     m_setRoundFinishedTimer = nullptr;
221 }
222 
setGameScene(GameScene * p_gameScene)223 void Game::setGameScene(GameScene* p_gameScene)
224 {
225     m_gameScene = p_gameScene;
226 }
227 
start()228 void Game::start()
229 {
230     // Restart the Game timer
231     m_timer->start();
232     m_state = RUNNING;
233     m_roundTimer->start();
234     Q_EMIT pauseChanged(false, false);
235 }
236 
pause(bool p_locked)237 void Game::pause(bool p_locked)
238 {
239     // Stop the Game timer
240     m_timer->stop();
241     m_roundTimer->stop();
242     if (p_locked)
243     {
244         m_state = PAUSED_LOCKED;
245     }
246     else
247     {
248         m_state = PAUSED_UNLOCKED;
249     }
250     Q_EMIT pauseChanged(true, false);
251 }
252 
switchPause()253 void Game::switchPause()
254 {
255     // If the Game is not already paused
256     if (m_state == RUNNING)
257     {
258         // Pause the Game
259         pause();
260         Q_EMIT pauseChanged(true, true);
261     }
262     // If the Game is already paused
263     else
264     {
265         // Resume the Game
266         start();
267         Q_EMIT pauseChanged(false, true);
268     }
269 }
270 
getPlayers() const271 QList<Player*> Game::getPlayers() const
272 {
273     return m_players;
274 }
275 
getTimer() const276 QTimer* Game::getTimer() const
277 {
278     return m_timer;
279 }
280 
getRemainingTime() const281 int Game::getRemainingTime() const
282 {
283     return m_remainingTime;
284 }
285 
getArena() const286 Arena* Game::getArena() const
287 {
288     return m_arena;
289 }
290 
isPaused() const291 bool Game::isPaused() const
292 {
293     return (m_state != RUNNING);
294 }
295 
getGameOver() const296 bool Game::getGameOver() const
297 {
298     return m_gameOver;
299 }
300 
getWinner() const301 QString Game::getWinner() const
302 {
303     return m_strWinner;
304 }
305 
getWinPoints() const306 int Game::getWinPoints() const
307 {
308     return m_winPoints;
309 }
310 
getBonus() const311 QList<Bonus*> Game::getBonus() const
312 {
313     return m_bonus;
314 }
315 
createBonus()316 void Game::createBonus()
317 {
318     int nRemainingNeutralBonuses = 1;
319     for(int nQuarter = 0; nQuarter < 4; nQuarter++)
320     {
321         Bonus* bonus;
322         int nBonusCount = static_cast<int>(0.3 * m_blocks.size() / 4);
323         int nBadBonusCount = static_cast<int>(0.1 * m_blocks.size() / 4);
324         int nNeutralBonusCount = granatier::RNG::fromRange(0, nRemainingNeutralBonuses);
325         QList<Granatier::Bonus::Type> bonusTypeList;
326         Granatier::Bonus::Type bonusType;
327         int nFullSize = m_blocks.size();
328         int nQuarterSize = (nQuarter < 3 ? nFullSize / 4 : nFullSize - 3 * (nFullSize / 4));
329 
330         for (int i = 0; i < nQuarterSize; ++i)
331         {
332             if(i < nBonusCount)
333             {
334                 constexpr int NumberOfBonuses = 6;
335                 switch (granatier::RNG::fromRange(0, NumberOfBonuses-1))
336                 {
337                     case 0: bonusType = Granatier::Bonus::SPEED;
338                             break;
339                     case 1: bonusType = Granatier::Bonus::BOMB;
340                             break;
341                     case 2: bonusType = Granatier::Bonus::POWER;
342                             break;
343                     case 3: bonusType = Granatier::Bonus::SHIELD;
344                             break;
345                     case 4: bonusType = Granatier::Bonus::THROW;
346                             break;
347                     case 5: bonusType = Granatier::Bonus::KICK;
348                             break;
349                     default: bonusType = Granatier::Bonus::SPEED;
350                 }
351             }
352             else if (i-nBonusCount < nBadBonusCount)
353             {
354                 constexpr int NumberOfBadBonuses = 5;
355                 switch (granatier::RNG::fromRange(0, NumberOfBadBonuses-1))
356                 {
357                     case 0: bonusType = Granatier::Bonus::HYPERACTIVE;
358                             break;
359                     case 1: bonusType = Granatier::Bonus::SLOW;
360                             break;
361                     case 2: bonusType = Granatier::Bonus::MIRROR;
362                             break;
363                     case 3: bonusType = Granatier::Bonus::SCATTY;
364                             break;
365                     case 4: bonusType = Granatier::Bonus::RESTRAIN;
366                             break;
367                     default: bonusType = Granatier::Bonus::HYPERACTIVE;
368                 }
369             }
370             else if(i-nBonusCount-nBadBonusCount < nNeutralBonusCount)
371             {
372                 bonusType = Granatier::Bonus::RESURRECT;
373                 nRemainingNeutralBonuses--;
374             }
375             else {
376                 bonusType = Granatier::Bonus::NONE;
377             }
378 
379             bonusTypeList.append(bonusType);
380         }
381 
382         int nShuffle;
383         for (int i = 0; i < nQuarterSize; ++i)
384         {
385             nShuffle = granatier::RNG::fromRange(0, nQuarterSize-1);
386 
387             bonusTypeList.swapItemsAt(i, nShuffle);
388         }
389 
390         for (int i = 0; i < nQuarterSize; ++i)
391         {
392             if(bonusTypeList[i] != Granatier::Bonus::NONE)
393             {
394                 int nIndex = nQuarter * (nFullSize/4) + i;
395                 bonus = new Bonus(m_blocks[nIndex]->getX(), m_blocks[nIndex]->getY(), m_arena, bonusTypeList[i]);
396                 m_bonus.append(bonus);
397                 m_blocks[nIndex]->setBonus(bonus);
398             }
399         }
400     }
401 }
402 
removeBonus(Bonus * bonus)403 void Game::removeBonus(Bonus* bonus)
404 {
405     m_bonus.removeAt(m_bonus.indexOf(bonus));
406     //do not delete the Bonus, because the ElementItem will delete it
407     if(m_soundEnabled && !bonus->isDestroyed())
408     {
409         m_soundBonus->start();
410     }
411 }
412 
setSoundsEnabled(bool p_enabled)413 void Game::setSoundsEnabled(bool p_enabled)
414 {
415     m_soundEnabled = p_enabled;
416     Settings::setSounds(p_enabled);
417     Settings::self()->save();
418 }
419 
initCharactersPosition()420 void Game::initCharactersPosition()
421 {
422     // If the timer is stopped, it means that collisions are already being handled
423     if (m_timer->isActive())
424     {
425         // At the beginning, the timer is stopped but the Game isn't paused (to allow keyPressedEvent detection)
426         m_timer->stop();
427         m_roundTimer->stop();
428         m_state = RUNNING;
429         // Initialize the Player coordinates
430         for(auto & player : m_players)
431         {
432             player->initCoordinate();
433             player->init();
434         }
435     }
436 }
437 
keyPressEvent(QKeyEvent * p_event)438 void Game::keyPressEvent(QKeyEvent* p_event)
439 {
440     if(p_event->isAutoRepeat())
441     {
442         return;
443     }
444 
445     // At the beginning or when paused, we start the timer when a key is pressed
446     if (!m_timer->isActive())
447     {
448         if(p_event->key() == Qt::Key_Space)
449         {
450             // If paused
451             if (m_state == PAUSED_UNLOCKED)
452             {
453                 switchPause();
454             }
455             else if (m_state == RUNNING)      // At the game beginning
456             {
457                 // Start the game
458                 m_timer->start();
459                 m_roundTimer->start();
460                 Q_EMIT gameStarted();
461             }
462             else if (m_state == PAUSED_LOCKED)
463             {
464                 // if the game is over, start a new game
465                 if (m_gameOver)
466                 {
467                     Q_EMIT gameOver();
468                     return;
469                 }
470                 else
471                 {
472                     m_gameScene->cleanUp();
473                     cleanUp();
474                     init();
475                     m_gameScene->init();
476                     for(auto & player : m_players)
477                     {
478                         player->resurrect();
479                     }
480                 }
481             }
482         }
483         return;
484     }
485     // Behaviour when the game has begun
486     switch (p_event->key())
487     {
488         case Qt::Key_P:
489         case Qt::Key_Escape:
490             switchPause();
491             return;
492         default:
493             break;
494     }
495 
496     //TODO: make signal
497     for(auto & player : m_players)
498     {
499         player->keyPressed(p_event);
500     }
501 }
502 
keyReleaseEvent(QKeyEvent * p_event)503 void Game::keyReleaseEvent(QKeyEvent* p_event)
504 {
505     if(p_event->isAutoRepeat())
506     {
507         return;
508     }
509     //TODO: make signal
510     for(auto & player : m_players)
511     {
512         player->keyReleased(p_event);
513     }
514 }
515 
update()516 void Game::update()
517 {
518     //update Bombs
519     for (auto & bomb : m_bombs)
520     {
521         bomb->updateMove();
522     }
523 
524     //update Player
525     for(auto & player : m_players)
526     {
527         player->updateMove();
528         player->emitGameUpdated();
529     }
530 }
531 
decrementRemainingRoundTime()532 void Game::decrementRemainingRoundTime()
533 {
534     m_remainingTime--;
535     if(m_remainingTime >= 0)
536     {
537         Q_EMIT infoChanged(Granatier::Info::TimeInfo);
538     }
539     else
540     {
541         if(m_remainingTime % 2 == 0)
542         {
543             //create bombs at randoms places
544             int nRow;
545             int nCol;
546             Granatier::Cell::Type cellType;
547             bool bFound = false;
548             do
549             {
550                 nRow = granatier::RNG::fromRange(0, m_arena->getNbRows()-1);
551                 nCol = granatier::RNG::fromRange(0, m_arena->getNbColumns()-1);
552                 cellType = m_arena->getCell(nRow, nCol).getType();
553                 if(cellType != Granatier::Cell::WALL && cellType != Granatier::Cell::HOLE && m_arena->getCell(nRow, nCol).isWalkable(nullptr))
554                 {
555                     bFound = true;
556                 }
557             }
558             while (!bFound);
559 
560             m_bombCount++;
561             Bomb* bomb = new Bomb((nCol + 0.5) * Granatier::CellSize, (nRow + 0.5) * Granatier::CellSize, m_arena, m_bombCount, 1000);    // time in ms
562             bomb->setBombPower(static_cast<int>(granatier::RNG::fromRange(1.0, 2.5)));
563             Q_EMIT bombCreated(bomb);
564             connect(bomb, &Bomb::bombDetonated, this, &Game::bombDetonated);
565             m_bombs.append(bomb);
566             if(m_remainingTime > -100 && m_roundTimer->interval() > 150)
567             {
568                 m_roundTimer->setInterval(m_roundTimer->interval() + m_remainingTime);
569             }
570             else if (m_roundTimer->interval() > 30)
571             {
572                 m_roundTimer->setInterval(m_roundTimer->interval() - 1);
573             }
574         }
575     }
576 }
577 
playerFalling()578 void Game::playerFalling()
579 {
580     if(m_soundEnabled)
581     {
582         m_soundFalling->start();
583     }
584 }
585 
playerDeath()586 void Game::playerDeath()
587 {
588     if(m_soundEnabled)
589     {
590         m_soundDie->start();
591     }
592 
593     //check if at most one player is alive if not already finished
594     if(!m_setRoundFinishedTimer->isActive())
595     {
596         int nPlayerAlive = 0;
597         for(auto & player : m_players)
598         {
599             if(player->isAlive())
600             {
601                 nPlayerAlive++;
602             }
603         }
604         if(nPlayerAlive <= 1)
605         {
606             //wait some time until the game stops
607             m_setRoundFinishedTimer->start(1500);
608         }
609    }
610 }
611 
resurrectBonusTaken()612 void Game::resurrectBonusTaken()
613 {
614     for(auto & player : m_players)
615     {
616         if(!(player->isAlive()))
617         {
618             player->resurrect();
619         }
620     }
621 }
622 
setRoundFinished()623 void Game::setRoundFinished()
624 {
625     int nPlayerAlive = 0;
626     int nIndex = 0;;
627     if(m_gameOver)
628     {
629         return;
630     }
631     for(int i = 0; i < m_players.length(); ++i)
632     {
633         if(m_players[i]->isAlive())
634         {
635             nPlayerAlive++;
636             nIndex = i;
637         }
638     }
639     //this check is needed, if in the meantime the resurrect bonus was taken
640     if (nPlayerAlive > 1)
641     {
642         return;
643     }
644 
645     if (nPlayerAlive == 1)
646     {
647         m_players[nIndex]->addPoint();
648     }
649 
650     pause(true);
651 
652     for(auto & player : m_players)
653     {
654         // check if a player reaches the win points
655         if (player->points() >= m_winPoints)
656         {
657             m_gameOver = true;
658             m_strWinner = player->getPlayerName();
659             break;
660         }
661     }
662     m_gameScene->showScore();
663 }
664 
createBomb(Player * player,qreal x,qreal y,bool newBomb,int throwDistance)665 void Game::createBomb(Player* player, qreal x, qreal y, bool newBomb, int throwDistance)
666 {
667     int col = m_arena->getColFromX(x);
668     int row = m_arena->getRowFromY(y);
669     if(col >= 0 && col < m_arena->getNbColumns() && row >= 0 && row < m_arena->getNbRows())
670     {
671         QList<Element*> bombElements =  m_arena->getCell(row, col).getElements(Granatier::Element::BOMB);
672         if (!bombElements.isEmpty())
673         {
674             if(player->hasThrowBomb() && throwDistance > 0)
675             {
676                 for(auto& element: bombElements)
677                 {
678                     dynamic_cast <Bomb*> (element)->setThrown(player->direction());
679                 }
680             }
681             return;
682         }
683     }
684 
685     if(!newBomb)
686     {
687         return;
688     }
689 
690     m_bombCount++;
691     Bomb* bomb = new Bomb((col + 0.5) * Granatier::CellSize, (row + 0.5) * Granatier::CellSize, m_arena, m_bombCount, 2500);    // time in ms
692     bomb->setBombPower(player->getBombPower());
693     Q_EMIT bombCreated(bomb);
694     connect(bomb, &Bomb::bombDetonated, this, &Game::bombDetonated);
695     connect(bomb, &Bomb::releaseBombArmory, player, &Player::slot_refillBombArmory);
696     m_bombs.append(bomb);
697     player->decrementBombArmory();
698 
699     if(m_soundEnabled)
700     {
701         m_soundPutBomb->start();
702     }
703 }
704 
removeBomb(Bomb * bomb)705 void Game::removeBomb(Bomb* bomb)
706 {
707     // Find the Bomb
708     int index = m_bombs.indexOf(bomb);
709     //remove the bomb
710     if(index != -1)
711     {
712         //do not delete the bomb because it will be deleted through the destructor of elementitem
713         m_bombs.removeAt(index);
714     }
715 }
716 
bombDetonated()717 void Game::bombDetonated()
718 {
719     if(m_soundEnabled)
720     {
721         m_soundExplode->start();
722     }
723 }
724 
blockDestroyed(const int row,const int col,Block * block)725 void Game::blockDestroyed(const int row, const int col, Block* block)
726 {
727     // find the Block
728     int index = m_blocks.indexOf(block);
729     // remove the Block
730     if(index != -1)
731     {
732         //do not delete the block because it will be deleted through the destructor of elementitem
733         m_arena->removeCellElement(row, col, block);
734     }
735 }
736