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 | |
35 | struct Collision |
36 | { |
37 | double square_distance; |
38 | QPointF line; |
39 | }; |
40 | |
41 | struct Theme : public KgTheme |
42 | { |
43 | Theme() : KgTheme("pictures/theme.desktop" ) |
44 | { |
45 | setGraphicsPath(KStandardDirs::locate("appdata" , "pictures/theme.svgz" )); |
46 | } |
47 | }; |
48 | |
49 | MainArea::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 | |
94 | void MainArea::enableSounds(bool p_enabled) |
95 | { |
96 | m_soundEnabled = p_enabled; |
97 | KollisionConfig::setEnableSounds(p_enabled); |
98 | KollisionConfig::self()->writeConfig(); |
99 | } |
100 | |
101 | Animation* 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 | |
127 | Animation* 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 | |
153 | void 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 | |
173 | double 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 | |
179 | void 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 | |
222 | void 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 | |
264 | void MainArea::setPauseAction(KAction* action) |
265 | { |
266 | m_pause_action = action; |
267 | } |
268 | |
269 | QPointF 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 | |
276 | QPointF 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 | |
282 | Ball* 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 | |
328 | bool 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 | |
336 | void 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 | |
358 | void 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 | |
526 | void 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 | |
540 | void 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 | |
559 | void MainArea::focusOutEvent(QFocusEvent*) |
560 | { |
561 | if (!m_paused) { |
562 | togglePause(); |
563 | } |
564 | } |
565 | |
566 | |