1 /*
2     SPDX-FileCopyrightText: 2007 Paolo Capriotti <p.capriotti@gmail.com>
3     SPDX-FileCopyrightText: 2010 Brian Croom <brian.s.croom@gmail.com>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "mainarea.h"
9 
10 #include <QApplication>
11 #include <QGraphicsView>
12 #include <QGraphicsSceneMouseEvent>
13 #include <QPainter>
14 #include <QAction>
15 
16 #include <KgDifficulty>
17 #include <KgTheme>
18 #include <KLocalizedString>
19 #include <QStandardPaths>
20 
21 #include "ball.h"
22 #include "kollisionconfig.h"
23 
24 #include <cmath>
25 #include <stdio.h>
26 
27 struct Collision
28 {
29     double square_distance;
30     QPointF line;
31 };
32 
33 struct Theme : public KgTheme
34 {
ThemeTheme35     Theme() : KgTheme("pictures/theme.desktop")
36     {
37         setGraphicsPath(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("pictures/theme.svgz")));
38     }
39 };
40 
MainArea()41 MainArea::MainArea()
42 : m_renderer(new Theme)
43 , m_man(nullptr)
44 , m_manBallDiameter(28)
45 , m_ballDiameter(28)
46 , m_death(false)
47 , m_game_over(false)
48 , m_paused(false)
49 , m_pauseTime(0)
50 , m_penalty(0)
51 , m_soundHitWall(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/hit_wall.ogg")))
52 , m_soundYouLose(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/you_lose.ogg")))
53 , m_soundBallLeaving(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/ball_leaving.ogg")))
54 , m_soundStart(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/start.ogg")))
55 , m_pauseAction(nullptr)
56 , m_random(new QRandomGenerator(QRandomGenerator::global()->generate()))
57 {
58 
59     // Initialize the sound state
60     enableSounds(KollisionConfig::enableSounds());
61 
62     increaseBallSize(KollisionConfig::increaseBallSize());
63 
64     m_size = 500;
65     QRect rect(0, 0, m_size, m_size);
66     setSceneRect(rect);
67 
68     m_timer.setInterval(20);
69     connect(&m_timer, &QTimer::timeout, this, &MainArea::tick);
70 
71     m_msgFont = QApplication::font();
72     m_msgFont.setPointSize(15);
73 
74     QPixmap pix(rect.size());
75     {
76         // draw gradient
77         QPainter p(&pix);
78         QColor color = palette().color(QPalette::Window);
79         QLinearGradient grad(QPointF(0, 0), QPointF(0, height()));
80         grad.setColorAt(0, color.lighter(115));
81         grad.setColorAt(1, color.darker(115));
82         p.fillRect(rect, grad);
83     }
84     setBackgroundBrush(pix);
85 
86     writeText(i18n("Welcome to Kollision\nClick to start a game"), false);
87 
88 }
89 
increaseBallSize(bool enable)90 void MainArea::increaseBallSize(bool enable)
91 {
92     m_increaseBallSize = enable;
93     KollisionConfig::setIncreaseBallSize(enable);
94     KollisionConfig::self()->save();
95 }
96 
enableSounds(bool enabled)97 void MainArea::enableSounds(bool enabled)
98 {
99     m_soundEnabled = enabled;
100     KollisionConfig::setEnableSounds(enabled);
101     KollisionConfig::self()->save();
102 }
103 
writeMessage(const QString & text)104 Animation* MainArea::writeMessage(const QString& text)
105 {
106     Message* message = new Message(text, m_msgFont, m_size);
107     message->setPosition(QPointF(m_size, m_size) / 2.0);
108     addItem(message);
109     message->setOpacityF(0.0);
110 
111     SpritePtr sprite(message);
112 
113     AnimationGroup* move = new AnimationGroup;
114     move->add(new FadeAnimation(sprite, 1.0, 0.0, 1500));
115     move->add(new MovementAnimation(sprite, sprite->position(), QPointF(0, -0.1), 1500));
116     AnimationSequence* sequence = new AnimationSequence;
117     sequence->add(new PauseAnimation(200));
118     sequence->add(new FadeAnimation(sprite, 0.0, 1.0, 1000));
119     sequence->add(new PauseAnimation(500));
120     sequence->add(move);
121 
122     m_animator.add(sequence);
123 
124     return sequence;
125 }
126 
writeText(const QString & text,bool fade)127 Animation* MainArea::writeText(const QString& text, bool fade)
128 {
129     m_welcomeMsg.clear();
130     const QStringList lines = text.split(QLatin1Char('\n'));
131     for (const QString &line : lines) {
132         m_welcomeMsg.append(
133             QExplicitlySharedDataPointer<Message>(new Message(line, m_msgFont, m_size)));
134     }
135     displayMessages(m_welcomeMsg);
136 
137     if (fade) {
138         AnimationGroup* anim = new AnimationGroup;
139         for (const auto& message : std::as_const(m_welcomeMsg)) {
140             message->setOpacityF(0.0);
141             anim->add(new FadeAnimation(message, 0.0, 1.0, 1000));
142         }
143 
144         m_animator.add(anim);
145 
146         return anim;
147     }
148     else {
149         return nullptr;
150     }
151 }
152 
displayMessages(const QList<QExplicitlySharedDataPointer<Message>> & messages)153 void MainArea::displayMessages(const QList<QExplicitlySharedDataPointer<Message> >& messages)
154 {
155     int totalHeight = 0;
156     for (const auto& message : messages) {
157       totalHeight += message->height();
158     }
159     QPointF pos(m_size / 2.0, (m_size - totalHeight) / 2.0);
160 
161     for (int i = 0; i < messages.size(); i++) {
162         QExplicitlySharedDataPointer<Message> msg = messages[i];
163         int halfHeight = msg->height() / 2;
164         pos.ry() += halfHeight;
165         msg->setPosition(pos);
166         msg->setZValue(10.0);
167         msg->show();
168         addItem(msg.data());
169         pos.ry() += halfHeight;
170     }
171 }
172 
radius() const173 double MainArea::radius() const
174 {
175     return m_ballDiameter / 2.0;
176 }
177 
setBallDiameter(int val)178 void MainArea::setBallDiameter(int val)
179 {
180     // Limits other balls' maximum diameter to the double of man ball's diameter.
181     if (m_ballDiameter < m_manBallDiameter * 2) {
182         m_ballDiameter = val;
183     }
184 }
185 
togglePause()186 void MainArea::togglePause()
187 {
188     if (!m_man) return;
189 
190     if (m_paused) {
191         m_paused = false;
192         m_timer.start();
193         m_welcomeMsg.clear();
194 
195         m_pauseTime += m_time.elapsed() - m_lastTime;
196         m_lastTime = m_time.elapsed();
197     }
198     else {
199         m_paused = true;
200         m_timer.stop();
201         QString shortcut = m_pauseAction ?
202           m_pauseAction->shortcut().toString() :
203           QStringLiteral("P");
204         writeText(i18n("Game paused\nClick or press %1 to resume", shortcut), false);
205 
206         if(m_lastGameTime >= 5) {
207             m_penalty += 5000;
208             m_lastGameTime -= 5;
209         }
210         else {
211             m_penalty += m_lastGameTime * 1000;
212             m_lastGameTime = 0;
213         }
214 
215         Q_EMIT changeGameTime(m_lastGameTime);
216     }
217 
218     m_man->setVisible(!m_paused);
219     for (Ball* ball : std::as_const(m_balls)) {
220         ball->setVisible(!m_paused);
221     }
222     for (Ball* ball : std::as_const(m_fading)) {
223         ball->setVisible(!m_paused);
224     }
225 
226     Q_EMIT pause(m_paused);
227 }
228 
start()229 void MainArea::start()
230 {
231     // reset ball size
232     m_ballDiameter = m_manBallDiameter;
233     m_man->setRenderSize(QSize(m_manBallDiameter, m_manBallDiameter));
234 
235     m_death = false;
236     m_game_over = false;
237 
238     switch (Kg::difficultyLevel()) {
239     case KgDifficultyLevel::Easy:
240         m_ball_timeout = 30;
241         break;
242     case KgDifficultyLevel::Medium:
243         m_ball_timeout = 25;
244         break;
245     case KgDifficultyLevel::Hard:
246     default:
247         m_ball_timeout = 20;
248         break;
249     }
250 
251     m_welcomeMsg.clear();
252 
253     addBall(QStringLiteral("red_ball"));
254     addBall(QStringLiteral("red_ball"));
255     addBall(QStringLiteral("red_ball"));
256     addBall(QStringLiteral("red_ball"));
257 
258     m_pauseTime = 0;
259     m_penalty = 0;
260     m_time.restart();
261     m_lastTime = 0;
262     m_lastGameTime = 0;
263 
264     m_timer.start();
265 
266     writeMessage(i18np("%1 ball", "%1 balls", 4));
267 
268     Q_EMIT changeGameTime(0);
269     Q_EMIT starting();
270 
271     if(m_soundEnabled)
272         m_soundStart.start();
273 }
274 
setPauseAction(QAction * action)275 void MainArea::setPauseAction(QAction * action)
276 {
277   m_pauseAction = action;
278 }
279 
randomPoint() const280 QPointF MainArea::randomPoint() const
281 {
282     const double x = m_random->bounded(m_size - radius() * 2) + radius();
283     const double y = m_random->bounded(m_size - radius() * 2) + radius();
284     return QPointF(x, y);
285 }
286 
randomDirection(double val) const287 QPointF MainArea::randomDirection(double val) const
288 {
289     const double angle = m_random->bounded(2 * M_PI);
290     return QPointF(val * sin(angle), val * cos(angle));
291 }
292 
addBall(const QString & id)293 Ball* MainArea::addBall(const QString& id)
294 {
295     QPoint pos;
296     for (bool done = false; !done; ) {
297         Collision tmp;
298 
299         done = true;
300         pos = randomPoint().toPoint();
301         for (Ball* ball : std::as_const(m_fading)) {
302             if (collide(pos, ball->position(), m_ballDiameter, m_ballDiameter, tmp)) {
303                 done = false;
304                 break;
305             }
306         }
307     }
308 
309     Ball* ball = new Ball(&m_renderer, id, static_cast<int>(radius()*2));
310     ball->setPosition(pos);
311     addItem(ball);
312 
313     // speed depends of game difficulty
314     double speed;
315     switch (Kg::difficultyLevel()) {
316     case KgDifficultyLevel::Easy:
317         speed = 0.2;
318         break;
319     case KgDifficultyLevel::Medium:
320         speed = 0.28;
321         break;
322     case KgDifficultyLevel::Hard:
323     default:
324         speed = 0.4;
325         break;
326     }
327     ball->setVelocity(randomDirection(speed));
328 
329     ball->setOpacityF(0.0);
330     ball->show();
331     m_fading.push_back(ball);
332 
333     // update statusbar
334     Q_EMIT changeBallNumber(m_balls.size() + m_fading.size());
335 
336     return ball;
337 }
338 
collide(const QPointF & a,const QPointF & b,double diamA,double diamB,Collision & collision)339 bool MainArea::collide(const QPointF& a, const QPointF& b, double diamA, double diamB, Collision& collision)
340 {
341     collision.line = b - a;
342     collision.square_distance = collision.line.x() * collision.line.x()
343                               + collision.line.y() * collision.line.y();
344 
345     return collision.square_distance <= diamA * diamB;
346 }
347 
abort()348 void MainArea::abort()
349 {
350     if (m_man) {
351         if (m_paused) {
352             togglePause();
353         }
354         m_death = true;
355 
356         m_man->setVelocity(QPointF(0, 0));
357         m_balls.push_back(m_man);
358         m_man = nullptr;
359         Q_EMIT changeState(false);
360 
361         for (Ball* fball : std::as_const(m_fading)) {
362             fball->setOpacityF(1.0);
363             fball->setVelocity(QPointF(0.0, 0.0));
364             m_balls.push_back(fball);
365         }
366         m_fading.clear();
367     }
368 }
369 
tick()370 void MainArea::tick()
371 {
372     if (!m_death && m_man && !m_paused) {
373         setManPosition(views().first()->mapFromGlobal(QCursor().pos()));
374     }
375 
376     int t = m_time.elapsed() - m_lastTime;
377     m_lastTime = m_time.elapsed();
378 
379     // compute game time && update statusbar
380     if ((m_time.elapsed() - m_pauseTime - m_penalty) / 1000 > m_lastGameTime) {
381         m_lastGameTime = (m_time.elapsed() - m_pauseTime - m_penalty) / 1000;
382         Q_EMIT changeGameTime(m_lastGameTime);
383     }
384 
385     Collision collision;
386 
387     // handle fade in
388     for (QList<Ball*>::iterator it = m_fading.begin();
389         it != m_fading.end(); ) {
390         (*it)->setOpacityF((*it)->opacityF() + t * 0.0005);
391         if ((*it)->opacityF() >= 1.0) {
392             m_balls.push_back(*it);
393             it = m_fading.erase(it);
394         }
395         else {
396             ++it;
397         }
398     }
399 
400     // handle deadly collisions
401     for (Ball* ball : std::as_const(m_balls)) {
402         if (m_man && collide(
403                 ball->position(),
404                 m_man->position(),
405                 m_ballDiameter,
406                 m_manBallDiameter,
407                 collision)) {
408             if(m_soundEnabled)
409                 m_soundYouLose.start();
410             abort();
411             break;
412         }
413     }
414 
415     // integrate
416     for (Ball* ball : std::as_const(m_balls)) {
417         // position
418         ball->setPosition(ball->position() +
419             ball->velocity() * t);
420 
421         // velocity
422         if (m_death) {
423             ball->setVelocity(ball->velocity() +
424                 QPointF(0, 0.001) * t);
425         }
426     }
427 
428     for (int i = 0; i < m_balls.size(); i++) {
429         Ball* ball = m_balls[i];
430 
431         QPointF vel = ball->velocity();
432         QPointF pos = ball->position();
433 
434         // handle collisions with borders
435         bool hit_wall = false;
436         if (pos.x() <= radius()) {
437             vel.setX(fabs(vel.x()));
438             pos.setX(2 * radius() - pos.x());
439             hit_wall = true;
440         }
441         if (pos.x() >= m_size - radius()) {
442             vel.setX(-fabs(vel.x()));
443             pos.setX(2 * (m_size - radius()) - pos.x());
444             hit_wall = true;
445         }
446         if (pos.y() <= radius()) {
447             vel.setY(fabs(vel.y()));
448             pos.setY(2 * radius() - pos.y());
449             hit_wall = true;
450         }
451         if (!m_death) {
452             if (pos.y() >= m_size - radius()) {
453                 vel.setY(-fabs(vel.y()));
454                 pos.setY(2 * (m_size - radius()) - pos.y());
455                 hit_wall = true;
456             }
457         }
458         if (hit_wall && m_soundEnabled) {
459             m_soundHitWall.start();
460         }
461 
462         // handle collisions with next balls
463         for (int j = i + 1; j < m_balls.size(); j++) {
464             Ball* other = m_balls[j];
465 
466             QPointF other_pos = other->position();
467 
468             if (collide(pos, other_pos, m_ballDiameter, m_ballDiameter, collision)) {
469 //                 onCollision();
470                 QPointF other_vel = other->velocity();
471 
472                 // compute the parallel component of the
473                 // velocity with respect to the collision line
474                 double v_par = vel.x() * collision.line.x()
475                              + vel.y() * collision.line.y();
476                 double w_par = other_vel.x() * collision.line.x()
477                              + other_vel.y() * collision.line.y();
478 
479                 // swap those components
480                 QPointF drift = collision.line * (w_par - v_par) /
481                                     collision.square_distance;
482                 vel += drift;
483                 other->setVelocity(other_vel - drift);
484 
485                 // adjust positions, reflecting along the collision
486                 // line as much as the amount of compenetration
487                 QPointF adj = collision.line *
488                     (2.0 * radius() /
489                     sqrt(collision.square_distance)
490                         - 1);
491                 pos -= adj;
492                 other->setPosition(other_pos + adj);
493             }
494 
495         }
496 
497         ball->setPosition(pos);
498         ball->setVelocity(vel);
499     }
500 
501     for (QList<Ball*>::iterator it = m_balls.begin();
502          it != m_balls.end(); ) {
503         Ball* ball = *it;
504         QPointF pos = ball->position();
505 
506         if (m_death && pos.y() >= height() + radius() + 10) {
507             if(m_soundEnabled)
508                 m_soundBallLeaving.start();
509             delete ball;
510             it = m_balls.erase(it);
511         }
512         else {
513             ++it;
514         }
515     }
516 
517     if (!m_death && m_time.elapsed() - m_pauseTime >= m_ball_timeout * 1000 *
518                                                        (m_balls.size() + m_fading.size() - 3)) {
519         if (m_increaseBallSize) {
520             //increase ball size by 4 units
521             setBallDiameter(m_ballDiameter + 4);
522             for (Ball* ball : std::as_const(m_balls)) {
523                 ball->setRenderSize(QSize(m_ballDiameter, m_ballDiameter));
524             }
525         }
526 
527         addBall(QStringLiteral("red_ball"));
528         writeMessage(i18np("%1 ball", "%1 balls", m_balls.size() + 1));
529     }
530 
531     if (m_death && m_balls.isEmpty() && m_fading.isEmpty()) {
532         m_game_over = true;
533         m_timer.stop();
534         int time = (m_time.restart() - m_pauseTime - m_penalty) / 1000;
535         QString text = i18np(
536             "GAME OVER\n"
537             "You survived for %1 second\n"
538             "Click to restart",
539             "GAME OVER\n"
540             "You survived for %1 seconds\n"
541             "Click to restart", time);
542         Q_EMIT gameOver(time);
543         Animation* a = writeText(text);
544         connect(this, &MainArea::starting, a, &Animation::stop);
545     }
546 }
547 
setManPosition(const QPointF & p)548 void MainArea::setManPosition(const QPointF& p)
549 {
550     Q_ASSERT(m_man);
551 
552     QPointF pos = p;
553 
554     if (pos.x() <= radius()) pos.setX(static_cast<int>(radius()));
555     if (pos.x() >= m_size - radius()) pos.setX(m_size - static_cast<int>(radius()));
556     if (pos.y() <= radius()) pos.setY(static_cast<int>(radius()));
557     if (pos.y() >= m_size - radius()) pos.setY(m_size - static_cast<int>(radius()));
558 
559     m_man->setPosition(pos);
560 }
561 
mousePressEvent(QGraphicsSceneMouseEvent * e)562 void MainArea::mousePressEvent(QGraphicsSceneMouseEvent* e)
563 {
564     if (!m_death || m_game_over) {
565         if (m_paused) {
566             togglePause();
567             setManPosition(e->scenePos());
568         }
569         else if (!m_man) {
570             m_man = new Ball(&m_renderer, QStringLiteral("blue_ball"), static_cast<int>(radius()*2));
571             m_man->setZValue(1.0);
572             setManPosition(e->scenePos());
573             addItem(m_man);
574 
575             start();
576             Q_EMIT changeState(true);
577         }
578     }
579 }
580 
focusOutEvent(QFocusEvent *)581 void MainArea::focusOutEvent(QFocusEvent*)
582 {
583     if (!m_paused) {
584         togglePause();
585     }
586 }
587 
588