File indexing completed on 2024-04-21 04:03:42
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 <KGameDifficulty> 0017 #include <KGameTheme> 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 KGameTheme 0034 { 0035 Theme() : KGameTheme("themes/default.desktop") 0036 { 0037 setGraphicsPath(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("themes/default.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 (KGameDifficulty::globalLevel()) { 0239 case KGameDifficultyLevel::Easy: 0240 m_ball_timeout = 30; 0241 break; 0242 case KGameDifficultyLevel::Medium: 0243 m_ball_timeout = 25; 0244 break; 0245 case KGameDifficultyLevel::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 (KGameDifficulty::globalLevel()) { 0316 case KGameDifficultyLevel::Easy: 0317 speed = 0.2; 0318 break; 0319 case KGameDifficultyLevel::Medium: 0320 speed = 0.28; 0321 break; 0322 case KGameDifficultyLevel::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 0588 #include "moc_mainarea.cpp"