File indexing completed on 2024-04-21 04:02:34

0001 /*
0002     This file is part of the KDE project "KLines"
0003 
0004     SPDX-FileCopyrightText: 2006 Dmitry Suzdalev <dimsuz@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "animator.h"
0010 #include "scene.h"
0011 #include "ballitem.h"
0012 #include "renderer.h"
0013 
0014 #include <math.h> // for pow, sqrt
0015 
0016 // Needed by A* pathfinding algorithm
0017 struct PathNode
0018 {
0019     FieldPos pos;
0020     PathNode *parent;
0021     int G;
0022     float H;
0023     float F;
0024     PathNode( FieldPos fpos, PathNode* p = nullptr, int g=0, int h=0 )
0025         : pos(fpos), parent(p), G(g), H(h), F(g+h) { }
0026 };
0027 
0028 // helper function - used in findPath()
0029 // returns:
0030 //         -1 - if position not found
0031 //         index of node in list if position is found
0032 static inline int indexOfNodeWithPos( FieldPos pos, const QList<PathNode*>& list )
0033 {
0034     for(int i=0;i<list.count(); ++i)
0035         if( list.at(i)->pos == pos )
0036             return i;
0037 
0038     return -1;
0039 }
0040 
0041 KLinesAnimator::KLinesAnimator( KLinesScene* scene )
0042     : m_scene(scene), m_movingBall(nullptr)
0043 {
0044     // timing & framing setup is done in corresponding animate* functions
0045 
0046     connect(&m_moveTimeLine, &QTimeLine::frameChanged, this, &KLinesAnimator::moveAnimationFrame);
0047     connect(&m_moveTimeLine, &QTimeLine::finished, this, &KLinesAnimator::moveFinished);
0048 
0049     m_removeTimeLine. setEasingCurve(QEasingCurve::Linear);
0050     connect(&m_removeTimeLine, &QTimeLine::frameChanged, this, &KLinesAnimator::removeAnimationFrame);
0051     connect(&m_removeTimeLine, &QTimeLine::finished, this, &KLinesAnimator::removeFinished);
0052 
0053     m_bornTimeLine. setEasingCurve(QEasingCurve::Linear);
0054     connect(&m_bornTimeLine, &QTimeLine::frameChanged, this, &KLinesAnimator::bornAnimationFrame);
0055     connect(&m_bornTimeLine, &QTimeLine::finished, this, &KLinesAnimator::slotBornFinished);
0056 }
0057 
0058 bool KLinesAnimator::isAnimating() const
0059 {
0060     return (m_bornTimeLine.state() == QTimeLine::Running
0061             || m_moveTimeLine.state() == QTimeLine::Running
0062             || m_removeTimeLine.state() == QTimeLine::Running);
0063 }
0064 
0065 bool KLinesAnimator::animateMove( FieldPos from, FieldPos to )
0066 {
0067     findPath(from, to);
0068 
0069     if(m_foundPath.isEmpty())
0070         return false;
0071 
0072     m_movingBall = m_scene->ballAt(from);
0073     m_movingBall->stopAnimation();
0074 
0075     int numPoints = m_foundPath.count();
0076     // there will be numPoints-1 intervals of
0077     // movement (interval=cell). We want each of them to take animDuration(MoveAnim) ms
0078     m_moveTimeLine.setDuration((numPoints-1)*KLinesRenderer::animDuration(KLinesRenderer::MoveAnim));
0079     // each interval will take cellSize frames
0080     m_moveTimeLine.setFrameRange(0, (numPoints-1)*KLinesRenderer::cellSize());
0081     m_moveTimeLine.setCurrentTime(0);
0082     m_moveTimeLine.start();
0083     return true;
0084 }
0085 
0086 void KLinesAnimator::animateRemove( const QList<BallItem*>& list )
0087 {
0088     m_moveTimeLine.stop();
0089     m_removeTimeLine.stop();
0090 
0091     if(list.isEmpty())
0092     {
0093         Q_EMIT removeFinished();
0094         return;
0095     }
0096 
0097     m_removedBalls = list;
0098 
0099     // called here (not in constructor), to stay in sync in case theme's reloaded
0100     m_removeTimeLine.setDuration(KLinesRenderer::animDuration(KLinesRenderer::DieAnim));
0101     // we setup here one 'empty' frame at the end, because without it
0102     // m_scene will delete 'burned' items in removeAnimFinished() slot so quickly
0103     // that last frame won't get shown in the scene
0104     m_removeTimeLine.setFrameRange(0, KLinesRenderer::frameCount(KLinesRenderer::DieAnim));
0105 
0106     m_removeTimeLine.start();
0107 }
0108 
0109 void KLinesAnimator::animateBorn( const QList<BallItem*>& list )
0110 {
0111     m_bornBalls = list;
0112     for (BallItem* ball : std::as_const(m_bornBalls)) {
0113         ball->setRenderSize(KLinesRenderer::cellExtent());
0114     }
0115 
0116     // called here (not in constructor), to stay in sync in case theme's reloaded
0117     m_bornTimeLine.setDuration(KLinesRenderer::animDuration(KLinesRenderer::BornAnim));
0118     m_bornTimeLine.setFrameRange(0, KLinesRenderer::frameCount(KLinesRenderer::BornAnim)-1);
0119 
0120     m_bornTimeLine.setCurrentTime( 0 );
0121     m_bornTimeLine.start();
0122 }
0123 
0124 void KLinesAnimator::moveAnimationFrame(int frame)
0125 {
0126     int cellSize = m_moveTimeLine.endFrame() / (m_foundPath.count()-1);
0127     int intervalNum = frame/cellSize;
0128 
0129     if(intervalNum == m_foundPath.count()-1)
0130     {
0131         m_movingBall->setPos(m_scene->fieldToPix(m_foundPath.last()));
0132         return;
0133     }
0134     // determine direction of movement on this interval
0135     int kx=0, ky=0;
0136 
0137     FieldPos from = m_foundPath.at(intervalNum);
0138     FieldPos to = m_foundPath.at(intervalNum+1);
0139 
0140     if( to.x - from.x > 0 )
0141         kx = 1;
0142     else if( to.x - from.x < 0 )
0143         kx = -1;
0144     else
0145         kx = 0;
0146 
0147     if( to.y - from.y > 0 )
0148         ky = 1;
0149     else if( to.y - from.y < 0 )
0150         ky = -1;
0151     else
0152         ky = 0;
0153 
0154     int frameWithinInterval = frame%cellSize;
0155     QPointF pos = m_scene->fieldToPix(from);
0156     m_movingBall->setPos( pos.x()+kx*frameWithinInterval,
0157                           pos.y()+ky*frameWithinInterval );
0158 }
0159 
0160 void KLinesAnimator::removeAnimationFrame(int frame)
0161 {
0162     if(frame == KLinesRenderer::frameCount(KLinesRenderer::DieAnim))
0163         return;
0164     for (BallItem* ball : std::as_const(m_removedBalls)) {
0165     ball->setSpriteKey(KLinesRenderer::animationFrameId( KLinesRenderer::DieAnim,
0166                                                                  ball->color(), frame) );
0167     }
0168 }
0169 
0170 void KLinesAnimator::bornAnimationFrame(int frame)
0171 {
0172     for (BallItem* ball : std::as_const(m_bornBalls)) {
0173         ball->setSpriteKey( KLinesRenderer::animationFrameId( KLinesRenderer::BornAnim,
0174                                                                  ball->color(), frame) );
0175     }
0176 }
0177 
0178 void KLinesAnimator::findPath( FieldPos from, FieldPos to )
0179 {
0180     // Implementation of A* pathfinding algorithm
0181     // Thanks to Patrick Lester for excellent tutorial on gamedev.net.
0182     // See https://www.gamedev.net/articles/programming/artificial-intelligence/a-pathfinding-for-beginners-r2003/
0183 
0184     QList<PathNode*> openList;
0185     QList<PathNode*> closedList;
0186 
0187     m_foundPath.clear();
0188 
0189     openList.append( new PathNode(from) );
0190 
0191     PathNode *curNode=nullptr;
0192     bool pathFound = false;
0193     // see exit conditions at the end of while loop below
0194     while(true)
0195     {
0196         // find the square with lowest F(=G+H) on the open list
0197         PathNode *minF = openList.at(0);
0198         for(int i=1; i<openList.count(); ++i)
0199             if( openList.at(i)->F < minF->F )
0200                 minF = openList.at(i);
0201 
0202         // move it to closed list
0203         closedList.append(minF);
0204         openList.removeAll(minF);
0205 
0206         curNode = minF;
0207 
0208         // for each of adjacent 4 squares (upper,lower, on the left and on the right)...
0209         QList<FieldPos> adjacentSquares;
0210         int x = curNode->pos.x;
0211         int y = curNode->pos.y;
0212         if( x != 0 ) adjacentSquares.append( FieldPos(x-1,y) );
0213         if( y != 0 ) adjacentSquares.append( FieldPos(x,y-1) );
0214         if( x != FIELD_SIZE-1 ) adjacentSquares.append( FieldPos(x+1,y) );
0215         if( y != FIELD_SIZE-1 ) adjacentSquares.append( FieldPos(x,y+1) );
0216 
0217         for (const FieldPos &pos : std::as_const(adjacentSquares)) {
0218             if( m_scene->ballAt(pos) != nullptr ) // skip non-walkable cells
0219                 continue;
0220 
0221             // skip if closed list contains this square
0222             if(indexOfNodeWithPos(pos, closedList) != -1)
0223                 continue;
0224 
0225             // search for node with position 'pos' in openList
0226             int idx = indexOfNodeWithPos(pos, openList);
0227             if(idx == -1) // not found
0228             {
0229                 PathNode *node = new PathNode( pos );
0230                 node->parent = curNode;
0231                 node->G = curNode->G + 10;
0232                 // h is manhattanLength from node to target square
0233                 node->H = sqrt( pow( (to.x - pos.x)*10, 2. ) + pow( (to.y - pos.y)*10, 2. ) );
0234                 node->F = node->G+node->H;
0235                 openList.append( node );
0236             }
0237             else
0238             {
0239                 PathNode *node = openList.at(idx);
0240                 // check if this path to square is better
0241                 if( curNode->G + 10 < node->G )
0242                 {
0243                     // yup, it's better, reparent and recalculate G,F
0244                     node->parent = curNode;
0245                     node->G = curNode->G + 10;
0246                     node->F = node->G + node->H;
0247                 }
0248             }
0249         } // for
0250 
0251         // exit conditions:
0252         // a) if closeList contains "to"
0253         // b) we can't find "to" in closedList and openlist is empty
0254         //    => no path exists
0255         int idx = indexOfNodeWithPos(to, closedList);
0256         if(idx != -1)
0257         {
0258             pathFound = true;
0259             // let's save last node in curNode variable
0260             curNode = closedList.at(idx);
0261             break; // while
0262         }
0263         else if(openList.isEmpty())
0264         {
0265             pathFound = false;
0266             break;
0267         }
0268     }
0269 
0270     if(pathFound)
0271     {
0272         // restoring path starting from last node:
0273         PathNode* node = curNode;
0274         while(node)
0275         {
0276             m_foundPath.prepend( node->pos );
0277             node = node->parent;
0278         }
0279     }
0280 
0281     // cleanup
0282     qDeleteAll( openList );
0283     qDeleteAll( closedList );
0284 }
0285 
0286 void KLinesAnimator::startGameOverAnimation()
0287 {
0288     blockSignals(true);
0289     QList<BallItem*> balls;
0290     const QList<QGraphicsItem*> items = m_scene->items();
0291     BallItem *ball=nullptr;
0292     for (QGraphicsItem* item : items) {
0293         ball = qgraphicsitem_cast<BallItem*>(item);
0294         if(ball)
0295             balls.append(ball);
0296     }
0297     animateRemove(balls);
0298 }
0299 
0300 void KLinesAnimator::stopGameOverAnimation()
0301 {
0302     blockSignals(false);
0303 }
0304 
0305 void KLinesAnimator::slotBornFinished()
0306 {
0307     for (BallItem* ball : std::as_const(m_bornBalls)) {
0308     ball->setColor(ball->color(), true);
0309     }
0310     Q_EMIT bornFinished();
0311 }
0312 
0313 #include "moc_animator.cpp"