File indexing completed on 2024-04-28 04:01:51

0001 /*
0002     SPDX-FileCopyrightText: 2007-2008 Thomas Gallinari <tg8187@yahoo.fr>
0003     SPDX-FileCopyrightText: 2007-2008 Pierre-Benoit Besse <besse@gmail.com>
0004     SPDX-FileCopyrightText: 2007-2008 Alexandre Galinier <alex.galinier@hotmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "game.h"
0010 #include "kapmanparser.h"
0011 #include "settings.h"
0012 
0013 #include <KGameDifficulty>
0014 #include <QFile>
0015 #include <QStandardPaths>
0016 
0017 const int Game::FPS = 40;
0018 int Game::s_bonusDuration;
0019 int Game::s_preyStateDuration;
0020 qreal Game::s_durationRatio;
0021 
0022 Game::Game()
0023     : m_isCheater(false)
0024     , m_lives(3)
0025     , m_points(0)
0026     , m_level(1)
0027     , m_nbEatenGhosts(0)
0028     , m_soundGameOver(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kapman/gameover.ogg")))
0029     , m_soundGhost(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kapman/ghost.ogg")))
0030     , m_soundGainLife(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kapman/life.ogg")))
0031     , m_soundEnergizer(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kapman/energizer.ogg")))
0032     , m_soundBonus(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kapman/bonus.ogg")))
0033     , m_soundPill(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kapman/pill.ogg")))
0034     , m_soundLevelUp(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kapman/levelup.ogg")))
0035 {
0036     // Initialize the sound state
0037     setSoundsEnabled(Settings::sounds());
0038 
0039     // Timers for medium difficulty
0040     s_bonusDuration = 7000;
0041     s_preyStateDuration = 10000;
0042     // Difference ratio between low/high and medium speed
0043     s_durationRatio = 1.0;
0044 
0045     // Tells the KGameDifficulty singleton that the game is not running
0046     KGameDifficulty::global()->setGameRunning(false);
0047 
0048     // Create the Maze instance
0049     m_maze = new Maze();
0050     connect(m_maze, &Maze::allElementsEaten, this, &Game::nextLevel);
0051     // Create the parser that will parse the XML file in order to initialize the Maze instance
0052     // This also creates all the characters
0053     KapmanParser kapmanParser(this);
0054     // Set the XML file as input source for the parser
0055     QFile mazeXmlFile(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("defaultmaze.xml")));
0056     mazeXmlFile.open(QIODevice::ReadOnly);
0057     kapmanParser.parse(&mazeXmlFile);
0058 
0059     connect(m_kapman, &Kapman::sWinPoints, this, &Game::winPoints);
0060 
0061     // Initialize the characters speed timers duration considering the difficulty level
0062     switch (KGameDifficulty::globalLevel()) {
0063     case KGameDifficultyLevel::Easy:
0064         // Ratio low/medium speed
0065         s_durationRatio = Character::MEDIUM_SPEED / Character::LOW_SPEED;
0066         break;
0067     case KGameDifficultyLevel::Medium:
0068         s_durationRatio = 1;
0069         break;
0070     case KGameDifficultyLevel::Hard:
0071         // Ratio high/medium speed
0072         s_durationRatio = Character::MEDIUM_SPEED / Character::HIGH_SPEED;
0073         break;
0074     default:
0075         break;
0076     }
0077 
0078     for (int i = 0; i < m_ghosts.size(); ++i) {
0079         connect(m_ghosts[i], &Ghost::lifeLost, this, &Game::kapmanDeath);
0080         connect(m_ghosts[i], &Ghost::ghostEaten, this, &Game::ghostDeath);
0081         // Initialize the ghosts speed and the ghost speed increase considering the characters speed
0082         m_ghosts[i]->initSpeedInc();
0083     }
0084     m_kapman->initSpeedInc();
0085 
0086     // Initialize Bonus timer from the difficulty level
0087     m_bonusTimer = new QTimer(this);
0088     m_bonusTimer->setInterval((int)(s_bonusDuration * s_durationRatio));
0089     m_bonusTimer->setSingleShot(true);
0090     connect(m_bonusTimer, &QTimer::timeout, this, &Game::hideBonus);
0091     // Initialize the Preys timer from the difficulty level
0092     m_preyTimer = new QTimer(this);
0093     m_preyTimer->setInterval((int)(s_preyStateDuration * s_durationRatio));
0094     m_preyTimer->setSingleShot(true);
0095     connect(m_preyTimer, &QTimer::timeout, this, &Game::endPreyState);
0096 
0097     // Start the Game timer
0098     m_timer = new QTimer(this);
0099     m_timer->setInterval(int(1000 / Game::FPS));
0100     connect(m_timer, &QTimer::timeout, this, &Game::update);
0101     m_timer->start();
0102     m_state = RUNNING;
0103     // Init the characters coordinates on the Maze
0104     initCharactersPosition();
0105 }
0106 
0107 Game::~Game()
0108 {
0109     delete m_timer;
0110     delete m_bonusTimer;
0111     delete m_maze;
0112     delete m_kapman;
0113     for (int i = 0; i < m_ghosts.size(); ++i) {
0114         delete m_ghosts[i];
0115     }
0116     delete m_bonus;
0117 }
0118 
0119 void Game::start()
0120 {
0121     // Restart the Game timer
0122     m_timer->start();
0123     m_bonusTimer->start();
0124     m_preyTimer->start();
0125     m_state = RUNNING;
0126     Q_EMIT pauseChanged(false, false);
0127 }
0128 
0129 void Game::pause(bool p_locked)
0130 {
0131     // Stop the Game timer
0132     m_timer->stop();
0133 
0134     if (m_bonusTimer->isActive()) {
0135         const auto remainingTime{m_bonusTimer->remainingTime()};
0136         m_bonusTimer->stop();
0137         m_bonusTimer->setInterval(remainingTime);
0138     }
0139 
0140     if (m_preyTimer->isActive()) {
0141         const auto remainingTime{m_preyTimer->remainingTime()};
0142         m_preyTimer->stop();
0143         m_preyTimer->setInterval(remainingTime);
0144     }
0145 
0146     if (p_locked) {
0147         m_state = PAUSED_LOCKED;
0148     } else {
0149         m_state = PAUSED_UNLOCKED;
0150     }
0151     Q_EMIT pauseChanged(true, false);
0152 }
0153 
0154 void Game::switchPause(bool p_locked)
0155 {
0156     // If the Game is not already paused
0157     if (m_state == RUNNING) {
0158         // Pause the Game
0159         pause(p_locked);
0160         Q_EMIT pauseChanged(true, true);
0161     }
0162     // If the Game is already paused
0163     else {
0164         // Resume the Game
0165         start();
0166         Q_EMIT pauseChanged(false, true);
0167     }
0168 }
0169 
0170 Kapman *Game::getKapman() const
0171 {
0172     return m_kapman;
0173 }
0174 
0175 QList<Ghost *> Game::getGhosts() const
0176 {
0177     return m_ghosts;
0178 }
0179 
0180 QTimer *Game::getTimer() const
0181 {
0182     return m_timer;
0183 }
0184 
0185 Maze *Game::getMaze() const
0186 {
0187     return m_maze;
0188 }
0189 
0190 bool Game::isPaused() const
0191 {
0192     return (m_state != RUNNING);
0193 }
0194 
0195 bool Game::isCheater() const
0196 {
0197     return m_isCheater;
0198 }
0199 
0200 int Game::getScore() const
0201 {
0202     return m_points;
0203 }
0204 int Game::getLives() const
0205 {
0206     return m_lives;
0207 }
0208 
0209 int Game::getLevel() const
0210 {
0211     return m_level;
0212 }
0213 
0214 void Game::setLevel(int p_level)
0215 {
0216     m_isCheater = true;
0217     m_level = p_level;
0218     m_maze->resetNbElem();
0219     m_timer->start(); // Needed to reinit character positions
0220     initCharactersPosition();
0221     for (int i = 0; i < m_ghosts.size(); ++i) {
0222         m_ghosts[i]->initSpeed();
0223     }
0224     m_kapman->initSpeed();
0225     for (int i = 0; i < m_level; ++i) {
0226         for (int j = 0; j < m_ghosts.size(); ++j) {
0227             m_ghosts[j]->increaseCharactersSpeed();
0228         }
0229         m_kapman->increaseCharactersSpeed();
0230     }
0231     setTimersDuration();
0232     m_bonus->setPoints(m_level * 100);
0233     Q_EMIT scoreChanged(m_points);
0234     Q_EMIT livesChanged(m_lives);
0235     Q_EMIT levelChanged(m_level);
0236     Q_EMIT pauseChanged(false, true);
0237     Q_EMIT levelStarted(true);
0238 }
0239 
0240 Bonus *Game::getBonus() const
0241 {
0242     return m_bonus;
0243 }
0244 
0245 void Game::createBonus(QPointF p_position)
0246 {
0247     m_bonus = new Bonus(qreal(Cell::SIZE * p_position.x()), qreal(Cell::SIZE * p_position.y()), m_maze, 100);
0248 }
0249 
0250 void Game::createKapman(QPointF p_position)
0251 {
0252     m_kapman = new Kapman(qreal(Cell::SIZE * p_position.x()), qreal(Cell::SIZE * p_position.y()), m_maze);
0253 }
0254 
0255 void Game::createGhost(QPointF p_position, const QString &p_imageId)
0256 {
0257     m_ghosts.append(new Ghost(qreal(Cell::SIZE * p_position.x()), qreal(Cell::SIZE * p_position.y()), p_imageId, m_maze));
0258 }
0259 
0260 void Game::initMaze(const int p_nbRows, const int p_nbColumns)
0261 {
0262     m_maze->init(p_nbRows, p_nbColumns);
0263 }
0264 
0265 void Game::setSoundsEnabled(bool p_enabled)
0266 {
0267     m_soundEnabled = p_enabled;
0268     Settings::setSounds(p_enabled);
0269     Settings::self()->save();
0270 }
0271 
0272 void Game::initCharactersPosition()
0273 {
0274     // If the timer is stopped, it means that collisions are already being handled
0275     if (m_timer->isActive()) {
0276         // At the beginning, the timer is stopped but the Game isn't paused (to allow keyPressedEvent detection)
0277         m_timer->stop();
0278         m_state = RUNNING;
0279         // Initialize Ghost coordinates and state
0280         m_ghosts[0]->initCoordinate();
0281         m_ghosts[1]->initCoordinate();
0282         m_ghosts[2]->initCoordinate();
0283         m_ghosts[3]->initCoordinate();
0284         m_kapman->initCoordinate();
0285         m_ghosts[0]->setState(Ghost::HUNTER);
0286         m_ghosts[1]->setState(Ghost::HUNTER);
0287         m_ghosts[2]->setState(Ghost::HUNTER);
0288         m_ghosts[3]->setState(Ghost::HUNTER);
0289         m_kapman->init();
0290         // Initialize the Pills & Energizers coordinates
0291         for (int i = 0; i < m_maze->getNbRows(); ++i) {
0292             for (int j = 0; j < m_maze->getNbColumns(); ++j) {
0293                 if (m_maze->getCell(i, j).getElement() != nullptr) {
0294                     m_maze->getCell(i, j).getElement()->setX(Cell::SIZE * (j + 0.5));
0295                     m_maze->getCell(i, j).getElement()->setY(Cell::SIZE * (i + 0.5));
0296                 }
0297             }
0298         }
0299     }
0300 }
0301 
0302 void Game::setTimersDuration()
0303 {
0304     // Updates the timers duration ratio with the ghosts speed
0305     s_durationRatio = Character::MEDIUM_SPEED / m_ghosts[0]->getNormalSpeed();
0306 
0307     setPreyTimerDuration();
0308     setBonusTimerDuration();
0309 }
0310 
0311 void Game::setBonusTimerDuration()
0312 {
0313     m_bonusTimer->setInterval((int)(s_bonusDuration * s_durationRatio));
0314 }
0315 
0316 void Game::setPreyTimerDuration()
0317 {
0318     m_preyTimer->setInterval((int)(s_preyStateDuration * s_durationRatio));
0319 }
0320 
0321 void Game::keyPressEvent(QKeyEvent *p_event)
0322 {
0323     // At the beginning or when paused, we start the timer when an arrow key is pressed
0324     if ((p_event->key() == Qt::Key_Up || p_event->key() == Qt::Key_Down || p_event->key() == Qt::Key_Left || p_event->key() == Qt::Key_Right)
0325         && !m_timer->isActive()) {
0326         // If paused
0327         if (m_state == PAUSED_UNLOCKED) {
0328             switchPause();
0329         } else if (m_state == RUNNING) { // At the game beginning
0330             // Start the game
0331             m_timer->start();
0332             Q_EMIT gameStarted();
0333         }
0334         // Tells the KGameDifficulty singleton that the game now runs
0335         KGameDifficulty::global()->setGameRunning(true);
0336     }
0337     // Behaviour when the game has begun
0338     switch (p_event->key()) {
0339     case Qt::Key_Up:
0340         if (m_state == RUNNING) {
0341             m_kapman->goUp();
0342         }
0343         break;
0344     case Qt::Key_Down:
0345         if (m_state == RUNNING) {
0346             m_kapman->goDown();
0347         }
0348         break;
0349     case Qt::Key_Right:
0350         if (m_state == RUNNING) {
0351             m_kapman->goRight();
0352         }
0353         break;
0354     case Qt::Key_Left:
0355         if (m_state == RUNNING) {
0356             m_kapman->goLeft();
0357         }
0358         break;
0359     case Qt::Key_P:
0360     case Qt::Key_Escape:
0361         switchPause();
0362         break;
0363     case Qt::Key_K:
0364         // Cheat code to get one more life
0365         if (p_event->modifiers() == (Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier)) {
0366             m_lives++;
0367             m_isCheater = true;
0368             Q_EMIT livesChanged(m_lives);
0369         }
0370         break;
0371     case Qt::Key_L:
0372         // Cheat code to go to the next level
0373         if (p_event->modifiers() == (Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier)) {
0374             m_isCheater = true;
0375             nextLevel();
0376         }
0377         break;
0378     default:
0379         break;
0380     }
0381 }
0382 
0383 void Game::update()
0384 {
0385     int curKapmanRow, curKapmanCol;
0386 
0387     // Check if the kapman is in the line of sight of a ghost
0388     curKapmanRow = m_maze->getRowFromY(m_kapman->getY());
0389     curKapmanCol = m_maze->getColFromX(m_kapman->getX());
0390 
0391     for (int i = 0; i < m_ghosts.size(); ++i) {
0392         if (m_ghosts[i]->getState() == Ghost::HUNTER && m_ghosts[i]->isInLineSight(m_kapman)) {
0393             m_ghosts[i]->updateMove(curKapmanRow, curKapmanCol);
0394         } else {
0395             m_ghosts[i]->updateMove();
0396         }
0397 
0398         const int startBlinkingDelay = s_preyStateDuration * s_durationRatio - 5 * (500 * s_durationRatio);
0399         if (m_preyTimer->remainingTime() < (m_preyTimer->interval() - startBlinkingDelay)) {
0400             m_ghosts[i]->setPreyStateAlmostOverEnabled(true);
0401         } else {
0402             m_ghosts[i]->setPreyStateAlmostOverEnabled(false);
0403         }
0404     }
0405     m_kapman->updateMove();
0406     m_kapman->emitGameUpdated();
0407 }
0408 
0409 void Game::kapmanDeath()
0410 {
0411     if (m_soundEnabled) {
0412         m_soundGameOver.start();
0413     }
0414 
0415     m_lives--;
0416     m_kapman->die();
0417     // Make a 2 seconds pause while the kapman is blinking
0418     pause(true);
0419     QTimer::singleShot(2500, this, &Game::resumeAfterKapmanDeath);
0420 }
0421 
0422 void Game::resumeAfterKapmanDeath()
0423 {
0424     Q_EMIT livesChanged(m_lives);
0425     // Start the timer
0426     start();
0427     // Remove a possible bonus
0428     Q_EMIT bonusOff();
0429     // If there are no lives left, we start a new game
0430     if (m_lives <= 0) {
0431         Q_EMIT gameOver();
0432     } else {
0433         Q_EMIT levelStarted(false);
0434         // Move all characters to their initial positions
0435         initCharactersPosition();
0436     }
0437 }
0438 
0439 void Game::ghostDeath(Ghost *p_ghost)
0440 {
0441     m_nbEatenGhosts++;
0442     p_ghost->setState(Ghost::EATEN);
0443     winPoints(p_ghost);
0444 }
0445 
0446 void Game::winPoints(Element *p_element)
0447 {
0448     // The value of won Points
0449     long wonPoints;
0450 
0451     // If the eaten element is a ghost, win 200 * number of eaten ghosts since the energizer was eaten
0452     if (p_element->getType() == Element::GHOST) {
0453         if (m_soundEnabled) {
0454             m_soundGhost.start();
0455         }
0456 
0457         // Get the position of the ghost
0458         qreal xPos = p_element->getX();
0459         qreal yPos = p_element->getY();
0460         // Add points to the score
0461         wonPoints = p_element->getPoints() * m_nbEatenGhosts;
0462         // Send to the scene the number of points to display and its position
0463         Q_EMIT pointsToDisplay(wonPoints, xPos, yPos);
0464     }
0465     // Else you just win the value of the element
0466     else {
0467         wonPoints = p_element->getPoints();
0468     }
0469 
0470     // Update of the points value
0471     m_points += wonPoints;
0472 
0473     // For each 10000 points we get a life more
0474     if (m_points / 10000 > (m_points - wonPoints) / 10000) {
0475         if (m_soundEnabled) {
0476             m_soundGainLife.start();
0477         }
0478 
0479         m_lives++;
0480         Q_EMIT livesChanged(m_lives);
0481     }
0482     // If the eaten element is an energyzer we change the ghosts state
0483     if (p_element->getType() == Element::ENERGYZER) {
0484         // We start the prey timer
0485         m_preyTimer->start();
0486 
0487         if (m_soundEnabled) {
0488             m_soundEnergizer.start();
0489         }
0490 
0491         for (int i = 0; i < m_ghosts.size(); ++i) {
0492             if (m_ghosts[i]->getState() != Ghost::EATEN) {
0493                 m_ghosts[i]->setState(Ghost::PREY);
0494             }
0495         }
0496         // Reset the number of eaten ghosts
0497         m_nbEatenGhosts = 0;
0498         Q_EMIT elementEaten(p_element->getX(), p_element->getY());
0499     } else if (p_element->getType() == Element::PILL) {
0500         if (m_soundEnabled) {
0501             m_soundPill.start();
0502         }
0503 
0504         Q_EMIT elementEaten(p_element->getX(), p_element->getY());
0505     } else if (p_element->getType() == Element::BONUS) {
0506         if (m_soundEnabled) {
0507             m_soundBonus.start();
0508         }
0509 
0510         // Get the position of the Bonus
0511         qreal xPos = p_element->getX();
0512         qreal yPos = p_element->getY();
0513 
0514         // Sends to the scene the number of points to display and its position
0515         Q_EMIT pointsToDisplay(wonPoints, xPos, yPos);
0516 
0517         Q_EMIT bonusOff();
0518     }
0519     // If 1/3 or 2/3 of the pills are eaten
0520     if (m_maze->getNbElem() == m_maze->getTotalNbElem() / 3 || m_maze->getNbElem() == (m_maze->getTotalNbElem() * 2 / 3)) {
0521         // Display the Bonus
0522         Q_EMIT bonusOn();
0523         m_bonusTimer->start();
0524     }
0525     Q_EMIT scoreChanged(m_points);
0526 }
0527 
0528 void Game::nextLevel()
0529 {
0530     if (m_soundEnabled) {
0531         m_soundLevelUp.start();
0532     }
0533 
0534     // Increment the level
0535     m_level++;
0536     // Initialize the maze items
0537     m_maze->resetNbElem();
0538     // Update Bonus
0539     m_bonus->setPoints(m_level * 100);
0540     // Move all characters to their initial positions
0541     initCharactersPosition();
0542     // Increase the ghosts speed
0543     for (int i = 0; i < m_ghosts.size(); ++i) {
0544         // Increase the ghosts speed increase
0545         m_ghosts[i]->increaseCharactersSpeed();
0546     }
0547     m_kapman->increaseCharactersSpeed();
0548     // Update the timers duration with the new speed
0549     setTimersDuration();
0550     // Update the score, level and lives labels
0551     Q_EMIT scoreChanged(m_points);
0552     Q_EMIT livesChanged(m_lives);
0553     Q_EMIT levelChanged(m_level);
0554     // Update the view
0555     Q_EMIT levelStarted(true);
0556 }
0557 
0558 void Game::hideBonus()
0559 {
0560     Q_EMIT bonusOff();
0561 
0562     setBonusTimerDuration();
0563 }
0564 
0565 void Game::endPreyState()
0566 {
0567     for (int i = 0; i < m_ghosts.size(); ++i) {
0568         if (m_ghosts[i]->getState() != Ghost::EATEN) {
0569             m_ghosts[i]->setState(Ghost::HUNTER);
0570         }
0571     }
0572 
0573     setPreyTimerDuration();
0574 }
0575 
0576 #include "moc_game.cpp"