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