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