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