File indexing completed on 2022-10-04 14:29:26

0001 /*
0002     SPDX-FileCopyrightText: 2007 Paolo Capriotti <p.capriotti@gmail.com>
0003     SPDX-FileCopyrightText: 2010 Brian Croom <brian.s.croom@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "mainarea.h"
0009 
0010 #include <QApplication>
0011 #include <QGraphicsView>
0012 #include <QGraphicsSceneMouseEvent>
0013 #include <QPainter>
0014 #include <QAction>
0015 
0016 #include <KgDifficulty>
0017 #include <KgTheme>
0018 #include <KLocalizedString>
0019 #include <QStandardPaths>
0020 
0021 #include "ball.h"
0022 #include "kollisionconfig.h"
0023 
0024 #include <cmath>
0025 #include <stdio.h>
0026 
0027 struct Collision
0028 {
0029     double square_distance;
0030     QPointF line;
0031 };
0032 
0033 struct Theme : public KgTheme
0034 {
0035     Theme() : KgTheme("pictures/theme.desktop")
0036     {
0037         setGraphicsPath(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("pictures/theme.svgz")));
0038     }
0039 };
0040 
0041 MainArea::MainArea()
0042 : m_renderer(new Theme)
0043 , m_man(nullptr)
0044 , m_manBallDiameter(28)
0045 , m_ballDiameter(28)
0046 , m_death(false)
0047 , m_game_over(false)
0048 , m_paused(false)
0049 , m_pauseTime(0)
0050 , m_penalty(0)
0051 , m_soundHitWall(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/hit_wall.ogg")))
0052 , m_soundYouLose(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/you_lose.ogg")))
0053 , m_soundBallLeaving(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/ball_leaving.ogg")))
0054 , m_soundStart(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/start.ogg")))
0055 , m_pauseAction(nullptr)
0056 , m_random(new QRandomGenerator(QRandomGenerator::global()->generate()))
0057 {
0058 
0059     // Initialize the sound state
0060     enableSounds(KollisionConfig::enableSounds());
0061 
0062     increaseBallSize(KollisionConfig::increaseBallSize());
0063 
0064     m_size = 500;
0065     QRect rect(0, 0, m_size, m_size);
0066     setSceneRect(rect);
0067 
0068     m_timer.setInterval(20);
0069     connect(&m_timer, &QTimer::timeout, this, &MainArea::tick);
0070 
0071     m_msgFont = QApplication::font();
0072     m_msgFont.setPointSize(15);
0073 
0074     QPixmap pix(rect.size());
0075     {
0076         // draw gradient
0077         QPainter p(&pix);
0078         QColor color = palette().color(QPalette::Window);
0079         QLinearGradient grad(QPointF(0, 0), QPointF(0, height()));
0080         grad.setColorAt(0, color.lighter(115));
0081         grad.setColorAt(1, color.darker(115));
0082         p.fillRect(rect, grad);
0083     }
0084     setBackgroundBrush(pix);
0085 
0086     writeText(i18n("Welcome to Kollision\nClick to start a game"), false);
0087 
0088 }
0089 
0090 void MainArea::increaseBallSize(bool enable)
0091 {
0092     m_increaseBallSize = enable;
0093     KollisionConfig::setIncreaseBallSize(enable);
0094     KollisionConfig::self()->save();
0095 }
0096 
0097 void MainArea::enableSounds(bool enabled)
0098 {
0099     m_soundEnabled = enabled;
0100     KollisionConfig::setEnableSounds(enabled);
0101     KollisionConfig::self()->save();
0102 }
0103 
0104 Animation* MainArea::writeMessage(const QString& text)
0105 {
0106     Message* message = new Message(text, m_msgFont, m_size);
0107     message->setPosition(QPointF(m_size, m_size) / 2.0);
0108     addItem(message);
0109     message->setOpacityF(0.0);
0110 
0111     SpritePtr sprite(message);
0112 
0113     AnimationGroup* move = new AnimationGroup;
0114     move->add(new FadeAnimation(sprite, 1.0, 0.0, 1500));
0115     move->add(new MovementAnimation(sprite, sprite->position(), QPointF(0, -0.1), 1500));
0116     AnimationSequence* sequence = new AnimationSequence;
0117     sequence->add(new PauseAnimation(200));
0118     sequence->add(new FadeAnimation(sprite, 0.0, 1.0, 1000));
0119     sequence->add(new PauseAnimation(500));
0120     sequence->add(move);
0121 
0122     m_animator.add(sequence);
0123 
0124     return sequence;
0125 }
0126 
0127 Animation* MainArea::writeText(const QString& text, bool fade)
0128 {
0129     m_welcomeMsg.clear();
0130     const QStringList lines = text.split(QLatin1Char('\n'));
0131     for (const QString &line : lines) {
0132         m_welcomeMsg.append(
0133             QExplicitlySharedDataPointer<Message>(new Message(line, m_msgFont, m_size)));
0134     }
0135     displayMessages(m_welcomeMsg);
0136 
0137     if (fade) {
0138         AnimationGroup* anim = new AnimationGroup;
0139         for (const auto& message : std::as_const(m_welcomeMsg)) {
0140             message->setOpacityF(0.0);
0141             anim->add(new FadeAnimation(message, 0.0, 1.0, 1000));
0142         }
0143 
0144         m_animator.add(anim);
0145 
0146         return anim;
0147     }
0148     else {
0149         return nullptr;
0150     }
0151 }
0152 
0153 void MainArea::displayMessages(const QList<QExplicitlySharedDataPointer<Message> >& messages)
0154 {
0155     int totalHeight = 0;
0156     for (const auto& message : messages) {
0157       totalHeight += message->height();
0158     }
0159     QPointF pos(m_size / 2.0, (m_size - totalHeight) / 2.0);
0160 
0161     for (int i = 0; i < messages.size(); i++) {
0162         QExplicitlySharedDataPointer<Message> msg = messages[i];
0163         int halfHeight = msg->height() / 2;
0164         pos.ry() += halfHeight;
0165         msg->setPosition(pos);
0166         msg->setZValue(10.0);
0167         msg->show();
0168         addItem(msg.data());
0169         pos.ry() += halfHeight;
0170     }
0171 }
0172 
0173 double MainArea::radius() const
0174 {
0175     return m_ballDiameter / 2.0;
0176 }
0177 
0178 void MainArea::setBallDiameter(int val)
0179 {
0180     // Limits other balls' maximum diameter to the double of man ball's diameter.
0181     if (m_ballDiameter < m_manBallDiameter * 2) {
0182         m_ballDiameter = val;
0183     }
0184 }
0185 
0186 void MainArea::togglePause()
0187 {
0188     if (!m_man) return;
0189 
0190     if (m_paused) {
0191         m_paused = false;
0192         m_timer.start();
0193         m_welcomeMsg.clear();
0194 
0195         m_pauseTime += m_time.elapsed() - m_lastTime;
0196         m_lastTime = m_time.elapsed();
0197     }
0198     else {
0199         m_paused = true;
0200         m_timer.stop();
0201         QString shortcut = m_pauseAction ?
0202           m_pauseAction->shortcut().toString() :
0203           QStringLiteral("P");
0204         writeText(i18n("Game paused\nClick or press %1 to resume", shortcut), false);
0205 
0206         if(m_lastGameTime >= 5) {
0207             m_penalty += 5000;
0208             m_lastGameTime -= 5;
0209         }
0210         else {
0211             m_penalty += m_lastGameTime * 1000;
0212             m_lastGameTime = 0;
0213         }
0214 
0215         Q_EMIT changeGameTime(m_lastGameTime);
0216     }
0217 
0218     m_man->setVisible(!m_paused);
0219     for (Ball* ball : std::as_const(m_balls)) {
0220         ball->setVisible(!m_paused);
0221     }
0222     for (Ball* ball : std::as_const(m_fading)) {
0223         ball->setVisible(!m_paused);
0224     }
0225 
0226     Q_EMIT pause(m_paused);
0227 }
0228 
0229 void MainArea::start()
0230 {
0231     // reset ball size
0232     m_ballDiameter = m_manBallDiameter;
0233     m_man->setRenderSize(QSize(m_manBallDiameter, m_manBallDiameter));
0234 
0235     m_death = false;
0236     m_game_over = false;
0237 
0238     switch (Kg::difficultyLevel()) {
0239     case KgDifficultyLevel::Easy:
0240         m_ball_timeout = 30;
0241         break;
0242     case KgDifficultyLevel::Medium:
0243         m_ball_timeout = 25;
0244         break;
0245     case KgDifficultyLevel::Hard:
0246     default:
0247         m_ball_timeout = 20;
0248         break;
0249     }
0250 
0251     m_welcomeMsg.clear();
0252 
0253     addBall(QStringLiteral("red_ball"));
0254     addBall(QStringLiteral("red_ball"));
0255     addBall(QStringLiteral("red_ball"));
0256     addBall(QStringLiteral("red_ball"));
0257 
0258     m_pauseTime = 0;
0259     m_penalty = 0;
0260     m_time.restart();
0261     m_lastTime = 0;
0262     m_lastGameTime = 0;
0263 
0264     m_timer.start();
0265 
0266     writeMessage(i18np("%1 ball", "%1 balls", 4));
0267 
0268     Q_EMIT changeGameTime(0);
0269     Q_EMIT starting();
0270 
0271     if(m_soundEnabled)
0272         m_soundStart.start();
0273 }
0274 
0275 void MainArea::setPauseAction(QAction * action)
0276 {
0277   m_pauseAction = action;
0278 }
0279 
0280 QPointF MainArea::randomPoint() const
0281 {
0282     const double x = m_random->bounded(m_size - radius() * 2) + radius();
0283     const double y = m_random->bounded(m_size - radius() * 2) + radius();
0284     return QPointF(x, y);
0285 }
0286 
0287 QPointF MainArea::randomDirection(double val) const
0288 {
0289     const double angle = m_random->bounded(2 * M_PI);
0290     return QPointF(val * sin(angle), val * cos(angle));
0291 }
0292 
0293 Ball* MainArea::addBall(const QString& id)
0294 {
0295     QPoint pos;
0296     for (bool done = false; !done; ) {
0297         Collision tmp;
0298 
0299         done = true;
0300         pos = randomPoint().toPoint();
0301         for (Ball* ball : std::as_const(m_fading)) {
0302             if (collide(pos, ball->position(), m_ballDiameter, m_ballDiameter, tmp)) {
0303                 done = false;
0304                 break;
0305             }
0306         }
0307     }
0308 
0309     Ball* ball = new Ball(&m_renderer, id, static_cast<int>(radius()*2));
0310     ball->setPosition(pos);
0311     addItem(ball);
0312 
0313     // speed depends of game difficulty
0314     double speed;
0315     switch (Kg::difficultyLevel()) {
0316     case KgDifficultyLevel::Easy:
0317         speed = 0.2;
0318         break;
0319     case KgDifficultyLevel::Medium:
0320         speed = 0.28;
0321         break;
0322     case KgDifficultyLevel::Hard:
0323     default:
0324         speed = 0.4;
0325         break;
0326     }
0327     ball->setVelocity(randomDirection(speed));
0328 
0329     ball->setOpacityF(0.0);
0330     ball->show();
0331     m_fading.push_back(ball);
0332 
0333     // update statusbar
0334     Q_EMIT changeBallNumber(m_balls.size() + m_fading.size());
0335 
0336     return ball;
0337 }
0338 
0339 bool MainArea::collide(const QPointF& a, const QPointF& b, double diamA, double diamB, Collision& collision)
0340 {
0341     collision.line = b - a;
0342     collision.square_distance = collision.line.x() * collision.line.x()
0343                               + collision.line.y() * collision.line.y();
0344 
0345     return collision.square_distance <= diamA * diamB;
0346 }
0347 
0348 void MainArea::abort()
0349 {
0350     if (m_man) {
0351         if (m_paused) {
0352             togglePause();
0353         }
0354         m_death = true;
0355 
0356         m_man->setVelocity(QPointF(0, 0));
0357         m_balls.push_back(m_man);
0358         m_man = nullptr;
0359         Q_EMIT changeState(false);
0360 
0361         for (Ball* fball : std::as_const(m_fading)) {
0362             fball->setOpacityF(1.0);
0363             fball->setVelocity(QPointF(0.0, 0.0));
0364             m_balls.push_back(fball);
0365         }
0366         m_fading.clear();
0367     }
0368 }
0369 
0370 void MainArea::tick()
0371 {
0372     if (!m_death && m_man && !m_paused) {
0373         setManPosition(views().first()->mapFromGlobal(QCursor().pos()));
0374     }
0375 
0376     int t = m_time.elapsed() - m_lastTime;
0377     m_lastTime = m_time.elapsed();
0378 
0379     // compute game time && update statusbar
0380     if ((m_time.elapsed() - m_pauseTime - m_penalty) / 1000 > m_lastGameTime) {
0381         m_lastGameTime = (m_time.elapsed() - m_pauseTime - m_penalty) / 1000;
0382         Q_EMIT changeGameTime(m_lastGameTime);
0383     }
0384 
0385     Collision collision;
0386 
0387     // handle fade in
0388     for (QList<Ball*>::iterator it = m_fading.begin();
0389         it != m_fading.end(); ) {
0390         (*it)->setOpacityF((*it)->opacityF() + t * 0.0005);
0391         if ((*it)->opacityF() >= 1.0) {
0392             m_balls.push_back(*it);
0393             it = m_fading.erase(it);
0394         }
0395         else {
0396             ++it;
0397         }
0398     }
0399 
0400     // handle deadly collisions
0401     for (Ball* ball : std::as_const(m_balls)) {
0402         if (m_man && collide(
0403                 ball->position(),
0404                 m_man->position(),
0405                 m_ballDiameter,
0406                 m_manBallDiameter,
0407                 collision)) {
0408             if(m_soundEnabled)
0409                 m_soundYouLose.start();
0410             abort();
0411             break;
0412         }
0413     }
0414 
0415     // integrate
0416     for (Ball* ball : std::as_const(m_balls)) {
0417         // position
0418         ball->setPosition(ball->position() +
0419             ball->velocity() * t);
0420 
0421         // velocity
0422         if (m_death) {
0423             ball->setVelocity(ball->velocity() +
0424                 QPointF(0, 0.001) * t);
0425         }
0426     }
0427 
0428     for (int i = 0; i < m_balls.size(); i++) {
0429         Ball* ball = m_balls[i];
0430 
0431         QPointF vel = ball->velocity();
0432         QPointF pos = ball->position();
0433 
0434         // handle collisions with borders
0435         bool hit_wall = false;
0436         if (pos.x() <= radius()) {
0437             vel.setX(fabs(vel.x()));
0438             pos.setX(2 * radius() - pos.x());
0439             hit_wall = true;
0440         }
0441         if (pos.x() >= m_size - radius()) {
0442             vel.setX(-fabs(vel.x()));
0443             pos.setX(2 * (m_size - radius()) - pos.x());
0444             hit_wall = true;
0445         }
0446         if (pos.y() <= radius()) {
0447             vel.setY(fabs(vel.y()));
0448             pos.setY(2 * radius() - pos.y());
0449             hit_wall = true;
0450         }
0451         if (!m_death) {
0452             if (pos.y() >= m_size - radius()) {
0453                 vel.setY(-fabs(vel.y()));
0454                 pos.setY(2 * (m_size - radius()) - pos.y());
0455                 hit_wall = true;
0456             }
0457         }
0458         if (hit_wall && m_soundEnabled) {
0459             m_soundHitWall.start();
0460         }
0461 
0462         // handle collisions with next balls
0463         for (int j = i + 1; j < m_balls.size(); j++) {
0464             Ball* other = m_balls[j];
0465 
0466             QPointF other_pos = other->position();
0467 
0468             if (collide(pos, other_pos, m_ballDiameter, m_ballDiameter, collision)) {
0469 //                 onCollision();
0470                 QPointF other_vel = other->velocity();
0471 
0472                 // compute the parallel component of the
0473                 // velocity with respect to the collision line
0474                 double v_par = vel.x() * collision.line.x()
0475                              + vel.y() * collision.line.y();
0476                 double w_par = other_vel.x() * collision.line.x()
0477                              + other_vel.y() * collision.line.y();
0478 
0479                 // swap those components
0480                 QPointF drift = collision.line * (w_par - v_par) /
0481                                     collision.square_distance;
0482                 vel += drift;
0483                 other->setVelocity(other_vel - drift);
0484 
0485                 // adjust positions, reflecting along the collision
0486                 // line as much as the amount of compenetration
0487                 QPointF adj = collision.line *
0488                     (2.0 * radius() /
0489                     sqrt(collision.square_distance)
0490                         - 1);
0491                 pos -= adj;
0492                 other->setPosition(other_pos + adj);
0493             }
0494 
0495         }
0496 
0497         ball->setPosition(pos);
0498         ball->setVelocity(vel);
0499     }
0500 
0501     for (QList<Ball*>::iterator it = m_balls.begin();
0502          it != m_balls.end(); ) {
0503         Ball* ball = *it;
0504         QPointF pos = ball->position();
0505 
0506         if (m_death && pos.y() >= height() + radius() + 10) {
0507             if(m_soundEnabled)
0508                 m_soundBallLeaving.start();
0509             delete ball;
0510             it = m_balls.erase(it);
0511         }
0512         else {
0513             ++it;
0514         }
0515     }
0516 
0517     if (!m_death && m_time.elapsed() - m_pauseTime >= m_ball_timeout * 1000 *
0518                                                        (m_balls.size() + m_fading.size() - 3)) {
0519         if (m_increaseBallSize) {
0520             //increase ball size by 4 units
0521             setBallDiameter(m_ballDiameter + 4);
0522             for (Ball* ball : std::as_const(m_balls)) {
0523                 ball->setRenderSize(QSize(m_ballDiameter, m_ballDiameter));
0524             }
0525         }
0526 
0527         addBall(QStringLiteral("red_ball"));
0528         writeMessage(i18np("%1 ball", "%1 balls", m_balls.size() + 1));
0529     }
0530 
0531     if (m_death && m_balls.isEmpty() && m_fading.isEmpty()) {
0532         m_game_over = true;
0533         m_timer.stop();
0534         int time = (m_time.restart() - m_pauseTime - m_penalty) / 1000;
0535         QString text = i18np(
0536             "GAME OVER\n"
0537             "You survived for %1 second\n"
0538             "Click to restart",
0539             "GAME OVER\n"
0540             "You survived for %1 seconds\n"
0541             "Click to restart", time);
0542         Q_EMIT gameOver(time);
0543         Animation* a = writeText(text);
0544         connect(this, &MainArea::starting, a, &Animation::stop);
0545     }
0546 }
0547 
0548 void MainArea::setManPosition(const QPointF& p)
0549 {
0550     Q_ASSERT(m_man);
0551 
0552     QPointF pos = p;
0553 
0554     if (pos.x() <= radius()) pos.setX(static_cast<int>(radius()));
0555     if (pos.x() >= m_size - radius()) pos.setX(m_size - static_cast<int>(radius()));
0556     if (pos.y() <= radius()) pos.setY(static_cast<int>(radius()));
0557     if (pos.y() >= m_size - radius()) pos.setY(m_size - static_cast<int>(radius()));
0558 
0559     m_man->setPosition(pos);
0560 }
0561 
0562 void MainArea::mousePressEvent(QGraphicsSceneMouseEvent* e)
0563 {
0564     if (!m_death || m_game_over) {
0565         if (m_paused) {
0566             togglePause();
0567             setManPosition(e->scenePos());
0568         }
0569         else if (!m_man) {
0570             m_man = new Ball(&m_renderer, QStringLiteral("blue_ball"), static_cast<int>(radius()*2));
0571             m_man->setZValue(1.0);
0572             setManPosition(e->scenePos());
0573             addItem(m_man);
0574 
0575             start();
0576             Q_EMIT changeState(true);
0577         }
0578     }
0579 }
0580 
0581 void MainArea::focusOutEvent(QFocusEvent*)
0582 {
0583     if (!m_paused) {
0584         togglePause();
0585     }
0586 }
0587