File indexing completed on 2024-05-12 04:04:32

0001 /*
0002     Copyright (C) 2002-2005, Jason Katz-Brown <jasonkb@mit.edu>
0003     Copyright 2010 Stefan Majewsky <majewsky@gmx.net>
0004 
0005     This program is free software; you can redistribute it and/or modify
0006     it under the terms of the GNU General Public License as published by
0007     the Free Software Foundation; either version 2 of the License, or
0008     (at your option) any later version.
0009 
0010     This program is distributed in the hope that it will be useful,
0011     but WITHOUT ANY WARRANTY; without even the implied warranty of
0012     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013     GNU General Public License for more details.
0014 
0015     You should have received a copy of the GNU General Public License
0016     along with this program; if not, write to the Free Software
0017     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
0018 */
0019 
0020 #include "game.h"
0021 #include "itemfactory.h"
0022 #include "kcomboboxdialog.h"
0023 #include "obstacles.h"
0024 #include "shape.h"
0025 
0026 #include "tagaro/board.h"
0027 
0028 #include <QHBoxLayout>
0029 #include <QApplication>
0030 #include <QCheckBox>
0031 #include <QFileDialog>
0032 #include <QLabel>
0033 #include <QMouseEvent>
0034 #include <QSpinBox>
0035 #include <QStandardPaths>
0036 #include <QTimer>
0037 #include <QUrl>
0038 #include <QRandomGenerator>
0039 #include <KGameRenderer>
0040 
0041 #include <KLineEdit>
0042 #include <KLocalizedString>
0043 #include <KMessageBox>
0044 
0045 #include <KGameTheme>
0046 #include <Box2D/Dynamics/b2Body.h>
0047 #include <Box2D/Dynamics/Contacts/b2Contact.h>
0048 #include <Box2D/Dynamics/b2Fixture.h>
0049 #include <Box2D/Dynamics/b2World.h>
0050 #include <Box2D/Dynamics/b2WorldCallbacks.h>
0051 
0052 inline QString makeGroup(int id, int hole, const QString &name, int x, int y)
0053 {
0054     return QStringLiteral("%1-%2@%3,%4|%5").arg(hole).arg(name).arg(x).arg(y).arg(id);
0055 }
0056 
0057 inline QString makeStateGroup(int id, const QString &name)
0058 {
0059     return QStringLiteral("%1|%2").arg(name).arg(id);
0060 }
0061 
0062 class KolfContactListener : public b2ContactListener
0063 {
0064     public:
0065         void PreSolve(b2Contact* contact, const b2Manifold* oldManifold) override
0066         {
0067             Q_UNUSED(oldManifold)
0068             CanvasItem* citemA = static_cast<CanvasItem*>(contact->GetFixtureA()->GetBody()->GetUserData());
0069             CanvasItem* citemB = static_cast<CanvasItem*>(contact->GetFixtureB()->GetBody()->GetUserData());
0070             if (!CanvasItem::mayCollide(citemA, citemB))
0071                 contact->SetEnabled(false);
0072         }
0073 };
0074 
0075 class KolfWorld : public b2World
0076 {
0077     public:
0078         KolfWorld()
0079             : b2World(b2Vec2(0, 0), true) //parameters: no gravity, objects are allowed to sleep
0080         {
0081             SetContactListener(&m_listener);
0082         }
0083     private:
0084         KolfContactListener m_listener;
0085 };
0086 
0087 class KolfTheme : public KGameTheme
0088 {
0089     public:
0090         KolfTheme() : KGameTheme("themes/default.desktop")
0091         {
0092             setGraphicsPath(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("themes/default.svgz")));
0093         }
0094 };
0095 
0096 class KolfRenderer : public KGameRenderer
0097 {
0098     public:
0099         KolfRenderer() : KGameRenderer(new KolfTheme)
0100         {
0101             setStrategyEnabled(KGameRenderer::UseDiskCache, false);
0102             setStrategyEnabled(KGameRenderer::UseRenderingThreads, false);
0103         }
0104 };
0105 
0106 Q_GLOBAL_STATIC(KolfRenderer, g_renderer)
0107 Q_GLOBAL_STATIC(KolfWorld, g_world)
0108 
0109 KGameRenderer* Kolf::renderer()
0110 {
0111     return g_renderer;
0112 }
0113 
0114 Tagaro::Board* Kolf::findBoard(QGraphicsItem* item_)
0115 {
0116     //This returns the toplevel board instance in which the given parent resides.
0117     return item_ ? dynamic_cast<Tagaro::Board*>(item_->topLevelItem()) : nullptr;
0118 }
0119 
0120 b2World* Kolf::world()
0121 {
0122     return g_world;
0123 }
0124 
0125 /////////////////////////
0126 
0127 Putter::Putter(QGraphicsItem* parent, b2World* world)
0128 : QGraphicsLineItem(parent)
0129 , CanvasItem(world)
0130 {
0131     setData(0, Rtti_Putter);
0132     setZBehavior(CanvasItem::FixedZValue, 10001);
0133     m_showGuideLine = true;
0134     oneDegree = M_PI / 180;
0135     guideLineLength = 9;
0136     putterWidth = 11;
0137     angle = 0;
0138 
0139     guideLine = new QGraphicsLineItem(this);
0140     guideLine->setPen(QPen(Qt::white));
0141     guideLine->setZValue(998.8);
0142 
0143     setPen(QPen(Qt::black, 4));
0144     maxAngle = 2 * M_PI;
0145 
0146     hideInfo();
0147 
0148     // this also sets Z
0149     resetAngles();
0150 }
0151 
0152 void Putter::showInfo()
0153 {
0154     guideLine->setVisible(isVisible());
0155 }
0156 
0157 void Putter::hideInfo()
0158 {
0159     guideLine->setVisible(m_showGuideLine? isVisible() : false);
0160 }
0161 
0162 void Putter::moveBy(double dx, double dy)
0163 {
0164     QGraphicsLineItem::moveBy(dx, dy);
0165     guideLine->setPos(x(), y());
0166     CanvasItem::moveBy(dx, dy);
0167 }
0168 
0169 void Putter::setShowGuideLine(bool yes)
0170 {
0171     m_showGuideLine = yes;
0172     setVisible(isVisible());
0173 }
0174 
0175 void Putter::setVisible(bool yes)
0176 {
0177     QGraphicsLineItem::setVisible(yes);
0178     guideLine->setVisible(m_showGuideLine? yes : false);
0179 }
0180 
0181 void Putter::setOrigin(double _x, double _y)
0182 {
0183     setVisible(true);
0184     setPos(_x, _y);
0185     guideLineLength = 9; //reset to default
0186     finishMe();
0187 }
0188 
0189 void Putter::setAngle(Ball *ball)
0190 {
0191     angle = angleMap.contains(ball)? angleMap[ball] : 0;
0192     finishMe();
0193 }
0194 
0195 void Putter::go(Direction d, Amount amount)
0196 {
0197     double addition = (amount == Amount_More? 6 * oneDegree : amount == Amount_Less? .5 * oneDegree : 2 * oneDegree);
0198 
0199     switch (d)
0200     {
0201         case Forwards:
0202             guideLineLength -= 1;
0203             guideLine->setVisible(false);
0204             break;
0205         case Backwards:
0206             guideLineLength += 1;
0207             guideLine->setVisible(false);
0208             break;
0209         case D_Left:
0210             angle += addition;
0211             if (angle > maxAngle)
0212                 angle -= maxAngle;
0213             break;
0214         case D_Right:
0215             angle -= addition;
0216             if (angle < 0)
0217                 angle = maxAngle - fabs(angle);
0218             break;
0219     }
0220 
0221     finishMe();
0222 }
0223 
0224 void Putter::finishMe()
0225 {
0226     midPoint.setX(cos(angle) * guideLineLength);
0227     midPoint.setY(-sin(angle) * guideLineLength);
0228 
0229     QPointF start;
0230     QPointF end;
0231 
0232     if (midPoint.y() || !midPoint.x())
0233     {
0234         start.setX(midPoint.x() - putterWidth * sin(angle));
0235         start.setY(midPoint.y() - putterWidth * cos(angle));
0236         end.setX(midPoint.x() + putterWidth * sin(angle));
0237         end.setY(midPoint.y() + putterWidth * cos(angle));
0238     }
0239     else
0240     {
0241         start.setX(midPoint.x());
0242         start.setY(midPoint.y() + putterWidth);
0243         end.setY(midPoint.y() - putterWidth);
0244         end.setX(midPoint.x());
0245     }
0246 
0247     guideLine->setLine(midPoint.x(), midPoint.y(), -cos(angle) * guideLineLength * 4, sin(angle) * guideLineLength * 4);
0248 
0249     setLine(start.x(), start.y(), end.x(), end.y());
0250 }
0251 
0252 /////////////////////////
0253 
0254 HoleConfig::HoleConfig(HoleInfo *holeInfo, QWidget *parent)
0255 : Config(parent)
0256 {
0257     this->holeInfo = holeInfo;
0258 
0259     QVBoxLayout *layout = new QVBoxLayout(this);
0260 
0261     QHBoxLayout *hlayout = new QHBoxLayout;
0262     layout->addLayout( hlayout );
0263     hlayout->addWidget(new QLabel(i18n("Course name: "), this));
0264     KLineEdit *nameEdit = new KLineEdit(holeInfo->untranslatedName(), this);
0265     hlayout->addWidget(nameEdit);
0266     connect(nameEdit, &KLineEdit::textChanged, this, &HoleConfig::nameChanged);
0267 
0268     hlayout = new QHBoxLayout;
0269     layout->addLayout( hlayout );
0270     hlayout->addWidget(new QLabel(i18n("Course author: "), this));
0271     KLineEdit *authorEdit = new KLineEdit(holeInfo->author(), this);
0272     hlayout->addWidget(authorEdit);
0273     connect(authorEdit, &KLineEdit::textChanged, this, &HoleConfig::authorChanged);
0274 
0275     layout->addStretch();
0276 
0277     hlayout = new QHBoxLayout;
0278     layout->addLayout( hlayout );
0279     hlayout->addWidget(new QLabel(i18n("Par:"), this));
0280     QSpinBox *par = new QSpinBox(this);
0281     par->setRange( 1, 15 );
0282     par->setSingleStep( 1 );
0283     par->setValue(holeInfo->par());
0284     hlayout->addWidget(par);
0285         connect(par, &QSpinBox::valueChanged, this,
0286                 &HoleConfig::parChanged);
0287         hlayout->addStretch();
0288 
0289     hlayout->addWidget(new QLabel(i18n("Maximum:"), this));
0290     QSpinBox *maxstrokes = new QSpinBox(this);
0291     maxstrokes->setRange( holeInfo->lowestMaxStrokes(), 30 );
0292     maxstrokes->setSingleStep( 1 );
0293     maxstrokes->setWhatsThis( i18n("Maximum number of strokes player can take on this hole."));
0294     maxstrokes->setToolTip( i18n("Maximum number of strokes"));
0295     maxstrokes->setSpecialValueText(i18n("Unlimited"));
0296     maxstrokes->setValue(holeInfo->maxStrokes());
0297     hlayout->addWidget(maxstrokes);
0298         connect(maxstrokes, &QSpinBox::valueChanged, this,
0299                 &HoleConfig::maxStrokesChanged);
0300 
0301         QCheckBox *check = new QCheckBox(i18n("Show border walls"), this);
0302     check->setChecked(holeInfo->borderWalls());
0303     layout->addWidget(check);
0304     connect(check, &QCheckBox::toggled, this, &HoleConfig::borderWallsChanged);
0305 }
0306 
0307 void HoleConfig::authorChanged(const QString &newauthor)
0308 {
0309     holeInfo->setAuthor(newauthor);
0310     changed();
0311 }
0312 
0313 void HoleConfig::nameChanged(const QString &newname)
0314 {
0315     holeInfo->setName(newname);
0316     holeInfo->setUntranslatedName(newname);
0317     changed();
0318 }
0319 
0320 void HoleConfig::parChanged(int newpar)
0321 {
0322     holeInfo->setPar(newpar);
0323     changed();
0324 }
0325 
0326 void HoleConfig::maxStrokesChanged(int newms)
0327 {
0328     holeInfo->setMaxStrokes(newms);
0329     changed();
0330 }
0331 
0332 void HoleConfig::borderWallsChanged(bool yes)
0333 {
0334     holeInfo->borderWallsChanged(yes);
0335     changed();
0336 }
0337 
0338 /////////////////////////
0339 
0340 StrokeCircle::StrokeCircle(QGraphicsItem *parent)
0341 : QGraphicsItem(parent)
0342 {
0343     dvalue = 0;
0344     dmax = 360;
0345     iwidth = 100;
0346     iheight = 100;
0347     ithickness = 8;
0348     setZValue(10000);
0349 
0350     setSize(QSizeF(80, 80));
0351     setThickness(8);
0352 }
0353 
0354 void StrokeCircle::setValue(double v)
0355 {
0356     dvalue = v;
0357     if (dvalue > dmax)
0358         dvalue = dmax;
0359 
0360     update();
0361 }
0362 
0363 double StrokeCircle::value()
0364 {
0365     return dvalue;
0366 }
0367 
0368 bool StrokeCircle::collidesWithItem(const QGraphicsItem*, Qt::ItemSelectionMode) const { return false; }
0369 
0370 QRectF StrokeCircle::boundingRect() const { return QRectF(x(), y(), iwidth, iheight); }
0371 
0372 void StrokeCircle::setMaxValue(double m)
0373 {
0374     dmax = m;
0375     if (dvalue > dmax)
0376         dvalue = dmax;
0377 }
0378 void StrokeCircle::setSize(const QSizeF& size)
0379 {
0380     if (size.width() > 0)
0381         iwidth = size.width();
0382     if (size.height() > 0)
0383         iheight = size.height();
0384 }
0385 void StrokeCircle::setThickness(double t)
0386 {
0387     if (t > 0)
0388         ithickness = t;
0389 }
0390 
0391 double StrokeCircle::thickness() const
0392 {
0393     return ithickness;
0394 }
0395 
0396 double StrokeCircle::width() const
0397 {
0398     return iwidth;
0399 }
0400 
0401 double StrokeCircle::height() const
0402 {
0403     return iheight;
0404 }
0405 
0406 void StrokeCircle::paint (QPainter *p, const QStyleOptionGraphicsItem *, QWidget * )
0407 {
0408     int al = (int)((dvalue * 360 * 16) / dmax);
0409     int length, deg;
0410     if (al < 0)
0411     {
0412         deg = 270 * 16;
0413         length = -al;
0414     }
0415     else if (al <= (270 * 16))
0416     {
0417         deg = 270 * 16 - al;
0418         length = al;
0419     }
0420     else
0421     {
0422         deg = (360 * 16) - (al - (270 * 16));
0423         length = al;
0424     }
0425 
0426     p->setBrush(QBrush(Qt::black, Qt::NoBrush));
0427     p->setPen(QPen(Qt::white, ithickness / 2));
0428     p->drawEllipse(QRectF(x() + ithickness / 2, y() + ithickness / 2, iwidth - ithickness, iheight - ithickness));
0429 
0430     if(dvalue>=0)
0431         p->setPen(QPen(QColor((int)((0xff * dvalue) / dmax), 0, (int)(0xff - (0xff * dvalue) / dmax)), ithickness));
0432     else
0433         p->setPen(QPen(QColor("black"), ithickness));
0434 
0435     p->drawArc(QRectF(x() + ithickness / 2, y() + ithickness / 2, iwidth - ithickness, iheight - ithickness), deg, length);
0436 
0437     p->setPen(QPen(Qt::white, 1));
0438     p->drawEllipse(QRectF(x(), y(), iwidth, iheight));
0439     p->drawEllipse(QRectF(x() + ithickness, y() + ithickness, iwidth - ithickness * 2, iheight - ithickness * 2));
0440     p->setPen(QPen(Qt::white, 3));
0441     p->drawLine(QPointF(x() + iwidth / 2, y() + iheight - ithickness * 1.5), QPointF(x() + iwidth / 2, y() + iheight));
0442     p->drawLine(QPointF(x() + iwidth / 4 - iwidth / 20, y() + iheight - iheight / 4 + iheight / 20), QPointF(x() + iwidth / 4 + iwidth / 20, y() + iheight - iheight / 4 - iheight / 20));
0443     p->drawLine(QPointF(x() + iwidth - iwidth / 4 + iwidth / 20, y() + iheight - iheight / 4 + iheight / 20), QPointF(x() + iwidth - iwidth / 4 - iwidth / 20, y() + iheight - iheight / 4 - iheight / 20));
0444 }
0445 /////////////////////////////////////////
0446 
0447 KolfGame::KolfGame(const Kolf::ItemFactory& factory, PlayerList *players, const QString &filename, QWidget *parent)
0448 : QGraphicsView(parent),
0449  m_factory(factory),
0450  m_soundBlackHole(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/blackhole.wav"))),
0451  m_soundBlackHoleEject(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/blackholeeject.wav"))),
0452  m_soundBlackHolePutIn(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/blackholeputin.wav"))),
0453  m_soundBumper(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/bumper.wav"))),
0454  m_soundHit(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/hit.wav"))),
0455  m_soundHoled(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/holed.wav"))),
0456  m_soundHoleINone(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/holeinone.wav"))),
0457  m_soundPuddle(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/puddle.wav"))),
0458  m_soundWall(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/wall.wav"))),
0459  m_soundWooHoo(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/woohoo.wav"))),
0460  holeInfo(g_world)
0461 {
0462     setRenderHint(QPainter::Antialiasing);
0463     // for mouse control
0464     setMouseTracking(true);
0465     viewport()->setMouseTracking(true);
0466     setFrameShape(NoFrame);
0467 
0468     regAdv = false;
0469     curHole = 0; // will get ++'d
0470     cfg = nullptr;
0471     setFilename(filename);
0472     this->players = players;
0473     curPlayer = players->end();
0474     curPlayer--; // will get ++'d to end and sent back
0475     // to beginning
0476     paused = false;
0477     modified = false;
0478     inPlay = false;
0479     putting = false;
0480     stroking = false;
0481     editing = false;
0482     strict = false;
0483     lastDelId = -1;
0484     m_showInfo = false;
0485     ballStateList.canUndo = false;
0486     dontAddStroke = false;
0487     addingNewHole = false;
0488     scoreboardHoles = 0;
0489     infoShown = false;
0490     m_useMouse = true;
0491     m_useAdvancedPutting = true;
0492     m_sound = true;
0493     m_ignoreEvents = false;
0494     highestHole = 0;
0495     recalcHighestHole = false;
0496     banner = nullptr;
0497 
0498     holeInfo.setGame(this);
0499     holeInfo.setAuthor(i18n("Course Author"));
0500     holeInfo.setName(i18n("Course Name"));
0501     holeInfo.setUntranslatedName(i18n("Course Name"));
0502     holeInfo.setMaxStrokes(10);
0503     holeInfo.borderWallsChanged(true);
0504 
0505     // width and height are the width and height of the scene
0506     // in easy storage
0507     width = 400;
0508     height = 400;
0509 
0510     margin = 10;
0511 
0512     setFocusPolicy(Qt::StrongFocus);
0513     setMinimumSize(width, height);
0514     QSizePolicy sizePolicy = QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
0515     setSizePolicy(sizePolicy);
0516 
0517     setContentsMargins(margin, margin, margin, margin);
0518 
0519     course = new Tagaro::Scene(Kolf::renderer(), QStringLiteral("grass"));
0520     course->setMainView(this); //this does this->setScene(course)
0521     courseBoard = new Tagaro::Board;
0522     courseBoard->setLogicalSize(QSizeF(400, 400));
0523     course->addItem(courseBoard);
0524 
0525     if( filename.contains( QLatin1String("intro") ) )
0526     {
0527         banner = new Tagaro::SpriteObjectItem(Kolf::renderer(), QStringLiteral("intro_foreground"), courseBoard);
0528         banner->setSize(400, 132);
0529         banner->setPos(0, 32);
0530         banner->setZValue(3); //on the height of a puddle (above slopes and sands, below any objects)
0531     }
0532 
0533     adjustSize();
0534 
0535     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
0536     {
0537         Ball* ball = (*it).ball();
0538         ball->setParentItem(courseBoard);
0539         m_topLevelQItems << ball;
0540         m_moveableQItems << ball;
0541     }
0542 
0543     QFont font = QApplication::font();
0544     font.setPixelSize(12);
0545 
0546     // create the advanced putting indicator
0547     strokeCircle = new StrokeCircle(courseBoard);
0548     strokeCircle->setPos(width - 90, height - 90);
0549     strokeCircle->setVisible(false);
0550     strokeCircle->setValue(0);
0551     strokeCircle->setMaxValue(360); 
0552 
0553     // whiteBall marks the spot of the whole whilst editing
0554     whiteBall = new Ball(courseBoard, g_world);
0555     whiteBall->setGame(this);
0556     whiteBall->setColor(Qt::white);
0557     whiteBall->setVisible(false);
0558     whiteBall->setDoDetect(false);
0559     m_topLevelQItems << whiteBall;
0560     m_moveableQItems << whiteBall;
0561 
0562     int highestLog = 0;
0563 
0564     // if players have scores from loaded game, move to last hole
0565     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
0566     {
0567         if ((int)(*it).scores().count() > highestLog)
0568             highestLog = (*it).scores().count();
0569 
0570         (*it).ball()->setGame(this);
0571     }
0572 
0573     // here only for saved games
0574     if (highestLog)
0575         curHole = highestLog;
0576 
0577     putter = new Putter(courseBoard, g_world);
0578 
0579     // border walls:
0580 
0581     // horiz
0582     addBorderWall(QPoint(margin, margin), QPoint(width - margin, margin));
0583     addBorderWall(QPoint(margin, height - margin - 1), QPoint(width - margin, height - margin - 1));
0584 
0585     // vert
0586     addBorderWall(QPoint(margin, margin), QPoint(margin, height - margin));
0587     addBorderWall(QPoint(width - margin - 1, margin), QPoint(width - margin - 1, height - margin));
0588 
0589     timer = new QTimer(this);
0590     connect(timer, &QTimer::timeout, this, &KolfGame::timeout);
0591     timerMsec = 300;
0592 
0593     fastTimer = new QTimer(this);
0594     connect(fastTimer, &QTimer::timeout, this, &KolfGame::fastTimeout);
0595     fastTimerMsec = 11;
0596 
0597     autoSaveTimer = new QTimer(this);
0598     connect(autoSaveTimer, &QTimer::timeout, this, &KolfGame::autoSaveTimeout);
0599     autoSaveMsec = 5 * 1000 * 60; // 5 min autosave
0600 
0601     // setUseAdvancedPutting() sets maxStrength!
0602     setUseAdvancedPutting(false);
0603 
0604     putting = false;
0605     putterTimer = new QTimer(this);
0606     connect(putterTimer, &QTimer::timeout, this, &KolfGame::putterTimeout);
0607     putterTimerMsec = 20;
0608 }
0609 
0610 void KolfGame::playSound(Sound soundType)
0611 {
0612     if (m_sound) {
0613         switch (soundType) {
0614             case Sound::BlackHole:
0615                 m_soundBlackHole.start();
0616                 break;
0617             case Sound::BlackHoleEject:
0618                 m_soundBlackHoleEject.start();
0619                 break;
0620             case Sound::BlackHolePutIn:
0621                 m_soundBlackHolePutIn.start();
0622                 break;
0623             case Sound::Bumper:
0624                 m_soundBumper.start();
0625                 break;
0626             case Sound::Hit:
0627                 m_soundHit.start();
0628                 break;
0629             case Sound::Holed:
0630                 m_soundHoled.start();
0631                 break;
0632             case Sound::HoleINone:
0633                 m_soundHoleINone.start();
0634                 break;
0635             case Sound::Puddle:
0636                 m_soundPuddle.start();
0637                 break;
0638             case Sound::Wall:
0639                 m_soundWall.start();
0640                 break;
0641             case Sound::WooHoo:
0642                 m_soundWooHoo.start();
0643                 break;
0644             default:
0645                 qWarning() << "There was a request to play an unknown sound.";
0646                 break;
0647         }
0648     }
0649 }
0650 
0651 void KolfGame::startFirstHole(int hole)
0652 {
0653     if (curHole > 0) // if there was saved game, sync scoreboard
0654         // with number of holes
0655     {
0656         for (; scoreboardHoles < curHole; ++scoreboardHoles)
0657         {
0658             cfgGroup = KConfigGroup(cfg->group(QStringLiteral("%1-hole@-50,-50|0").arg(scoreboardHoles + 1)));
0659             Q_EMIT newHole(cfgGroup.readEntry("par", 3));
0660         }
0661 
0662         // lets load all of the scores from saved game if there are any
0663         for (int hole = 1; hole <= curHole; ++hole)
0664             for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
0665                 Q_EMIT scoreChanged((*it).id(), hole, (*it).score(hole));
0666     }
0667 
0668     curHole = hole - 1;
0669 
0670     // this increments curHole, etc
0671     recalcHighestHole = true;
0672     startNextHole();
0673     paused = true;
0674     unPause();
0675 }
0676 
0677 void KolfGame::setFilename(const QString &filename)
0678 {
0679     this->filename = filename;
0680     delete cfg;
0681     cfg = new KConfig(filename, KConfig::NoGlobals);
0682 }
0683 
0684 KolfGame::~KolfGame()
0685 {
0686     const QList<QGraphicsItem*> itemsCopy(m_topLevelQItems); //this list will be modified soon, so take a copy
0687     for (QGraphicsItem* item : itemsCopy) {
0688         CanvasItem* citem = dynamic_cast<CanvasItem*>(item);
0689         delete citem;
0690     }
0691 
0692     delete cfg;
0693 }
0694 
0695 void KolfGame::setModified(bool mod)
0696 {
0697     modified = mod;
0698     Q_EMIT modifiedChanged(mod);
0699 }
0700 
0701 void KolfGame::pause()
0702 {
0703     if (paused)
0704     {
0705         // play along with people who call pause() again, instead of unPause()
0706         unPause();
0707         return;
0708     }
0709 
0710     paused = true;
0711     timer->stop();
0712     fastTimer->stop();
0713     putterTimer->stop();
0714 }
0715 
0716 void KolfGame::unPause()
0717 {
0718     if (!paused)
0719         return;
0720 
0721     paused = false;
0722 
0723     timer->start(timerMsec);
0724     fastTimer->start(fastTimerMsec);
0725 
0726     if (putting || stroking)
0727         putterTimer->start(putterTimerMsec);
0728 }
0729 
0730 void KolfGame::addBorderWall(const QPoint &start, const QPoint &end)
0731 {
0732     Kolf::Wall *wall = new Kolf::Wall(courseBoard, g_world);
0733     wall->setLine(QLineF(start, end));
0734     wall->setVisible(true);
0735     wall->setGame(this);
0736     //change Z value to something very high so that border walls
0737     //really keep the balls inside the course
0738     wall->setZBehavior(CanvasItem::FixedZValue, 10000);
0739     borderWalls.append(wall);
0740 }
0741 
0742 void KolfGame::handleMouseDoubleClickEvent(QMouseEvent *e)
0743 {
0744     // allow two fast single clicks
0745     handleMousePressEvent(e);
0746 }
0747 
0748 void KolfGame::handleMousePressEvent(QMouseEvent *e)
0749 {
0750     if (m_ignoreEvents)
0751         return;
0752 
0753     if (editing)
0754     {
0755         //at this point, QGV::mousePressEvent and thus the interaction
0756         //with overlays has already been done; we therefore know that
0757         //the user has clicked into free space
0758         setSelectedItem(nullptr);
0759         return;
0760     }
0761     else
0762     {
0763         if (m_useMouse)
0764         {
0765             if (!inPlay && e->button() == Qt::LeftButton)
0766                 puttPress();
0767             else if (e->button() == Qt::RightButton)
0768                 toggleShowInfo();
0769         }
0770     }
0771 
0772     setFocus();
0773 }
0774 
0775 QPoint KolfGame::viewportToViewport(const QPoint &p)
0776 {
0777     //convert viewport coordinates to board coordinates
0778     return courseBoard->deviceTransform(viewportTransform()).inverted().map(p);
0779 }
0780 
0781 // the following four functions are needed to handle both
0782 // border presses and regular in-course presses
0783 
0784 void KolfGame::mouseReleaseEvent(QMouseEvent * e)
0785 {
0786     e->setAccepted(false);
0787     QGraphicsView::mouseReleaseEvent(e);
0788     if (e->isAccepted())
0789         return;
0790 
0791     QMouseEvent fixedEvent (QEvent::MouseButtonRelease, viewportToViewport(e->position().toPoint()), e->globalPosition(), e->button(), e->buttons(), e->modifiers(), e->pointingDevice());
0792     handleMouseReleaseEvent(&fixedEvent);
0793     e->accept();
0794 }
0795 
0796 void KolfGame::mousePressEvent(QMouseEvent * e)
0797 {
0798     e->setAccepted(false);
0799     QGraphicsView::mousePressEvent(e);
0800     if (e->isAccepted())
0801         return;
0802 
0803     QMouseEvent fixedEvent (QEvent::MouseButtonPress, viewportToViewport(e->position().toPoint()), e->globalPosition(), e->button(), e->buttons(), e->modifiers(), e->pointingDevice());
0804     handleMousePressEvent(&fixedEvent);
0805     e->accept();
0806 }
0807 
0808 void KolfGame::mouseDoubleClickEvent(QMouseEvent * e)
0809 {
0810     e->setAccepted(false);
0811     QGraphicsView::mouseDoubleClickEvent(e);
0812     if (e->isAccepted())
0813         return;
0814 
0815     QMouseEvent fixedEvent (QEvent::MouseButtonDblClick, viewportToViewport(e->position().toPoint()), e->globalPosition(), e->button(), e->buttons(), e->modifiers(), e->pointingDevice());
0816     handleMouseDoubleClickEvent(&fixedEvent);
0817     e->accept();
0818 }
0819 
0820 void KolfGame::mouseMoveEvent(QMouseEvent * e)
0821 {
0822     e->setAccepted(false);
0823     QGraphicsView::mouseMoveEvent(e);
0824     if (e->isAccepted())
0825         return;
0826 
0827     QMouseEvent fixedEvent (QEvent::MouseMove, viewportToViewport(e->position().toPoint()), e->globalPosition(), e->button(), e->buttons(), e->modifiers(), e->pointingDevice());
0828     handleMouseMoveEvent(&fixedEvent);
0829     e->accept();
0830 }
0831 
0832 void KolfGame::handleMouseMoveEvent(QMouseEvent *e)
0833 {
0834     if (!editing && !inPlay && putter && !m_ignoreEvents)
0835     {
0836         // mouse moving of putter
0837         updateMouse();
0838         e->accept();
0839     }
0840 }
0841 
0842 void KolfGame::updateMouse()
0843 {
0844     // don't move putter if in advanced putting sequence
0845     if (!m_useMouse || ((stroking || putting) && m_useAdvancedPutting))
0846         return;
0847 
0848     const QPointF cursor = viewportToViewport(mapFromGlobal(QCursor::pos()));
0849     const QPointF ball((*curPlayer).ball()->x(), (*curPlayer).ball()->y());
0850     putter->setAngle(-Vector(cursor - ball).direction());
0851 }
0852 
0853 void KolfGame::handleMouseReleaseEvent(QMouseEvent *e)
0854 {
0855     setCursor(Qt::ArrowCursor);
0856 
0857     if (editing)
0858     {
0859         Q_EMIT newStatusText(QString());
0860     }
0861 
0862     if (m_ignoreEvents)
0863         return;
0864 
0865     if (!editing && m_useMouse)
0866     {
0867         if (!inPlay && e->button() == Qt::LeftButton)
0868             puttRelease();
0869         else if (e->button() == Qt::RightButton)
0870             toggleShowInfo();
0871     }
0872 
0873     setFocus();
0874 }
0875 
0876 void KolfGame::keyPressEvent(QKeyEvent *e)
0877 {
0878     if (inPlay || editing || m_ignoreEvents)
0879         return;
0880 
0881     switch (e->key())
0882     {
0883         case Qt::Key_Up:
0884             if (!e->isAutoRepeat())
0885                 toggleShowInfo();
0886             break;
0887 
0888         case Qt::Key_Escape:
0889             putting = false;
0890             stroking = false;
0891             finishStroking = false;
0892             strokeCircle->setVisible(false); 
0893             putterTimer->stop();
0894             putter->setOrigin((*curPlayer).ball()->x(), (*curPlayer).ball()->y());
0895             break;
0896 
0897         case Qt::Key_Left:
0898         case Qt::Key_Right:
0899             // don't move putter if in advanced putting sequence
0900             if ((!stroking && !putting) || !m_useAdvancedPutting)
0901                 putter->go(e->key() == Qt::Key_Left? D_Left : D_Right, e->modifiers() & Qt::ShiftModifier? Amount_More : e->modifiers() & Qt::ControlModifier? Amount_Less : Amount_Normal);
0902             break;
0903 
0904         case Qt::Key_Space: case Qt::Key_Down:
0905             puttPress();
0906             break;
0907 
0908         default:
0909             break;
0910     }
0911 }
0912 
0913 void KolfGame::toggleShowInfo()
0914 {
0915     setShowInfo(!m_showInfo);
0916 }
0917 
0918 void KolfGame::updateShowInfo()
0919 {
0920     setShowInfo(m_showInfo);
0921 }
0922 
0923 void KolfGame::setShowInfo(bool yes)
0924 {
0925     m_showInfo = yes;
0926     QList<QGraphicsItem*> infoItems;
0927     for (QGraphicsItem* qitem : std::as_const(m_topLevelQItems)) {
0928         CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
0929         if (citem)
0930             infoItems << citem->infoItems();
0931     }
0932     for (QGraphicsItem* qitem : std::as_const(infoItems))
0933         qitem->setVisible(m_showInfo);
0934 }
0935 
0936 void KolfGame::puttPress()
0937 {
0938     // Advanced putting: 1st click start putting sequence, 2nd determine strength, 3rd determine precision
0939 
0940     if (!putting && !stroking && !inPlay)
0941     {
0942         puttCount = 0;
0943         puttReverse = false;
0944         putting = true;
0945         stroking = false;
0946         strength = 0;
0947         if (m_useAdvancedPutting)
0948         {
0949             strokeCircle->setValue(0); 
0950             int pw = (int)(putter->line().x2() - putter->line().x1());
0951             if (pw < 0) pw = -pw;
0952             int px = (int)putter->x() + pw / 2;
0953             int py = (int)putter->y();
0954             if (px > width / 2 && py < height / 2) 
0955                 strokeCircle->setPos(px/2 - pw / 2 - 5 - strokeCircle->width()/2, py/2 + 5);
0956             else if (px > width / 2) 
0957                 strokeCircle->setPos(px/2 - pw / 2 - 5 - strokeCircle->width()/2, py/2 - 5 - strokeCircle->height()/2);
0958             else if (py < height / 2) 
0959                 strokeCircle->setPos(px/2 + pw / 2 + 5, py/2 + 5);
0960             else 
0961                 strokeCircle->setPos(px/2 + pw / 2 + 5, py/2 - 5 - strokeCircle->height()/2);
0962             strokeCircle->setVisible(true); 
0963         }
0964         putterTimer->start(putterTimerMsec);
0965     }
0966     else if (m_useAdvancedPutting && putting && !editing)
0967     {
0968         putting = false;
0969         stroking = true;
0970         puttReverse = false;
0971         finishStroking = false;
0972     }
0973     else if (m_useAdvancedPutting && stroking)
0974     {
0975         finishStroking = true;
0976         putterTimeout();
0977     }
0978 }
0979 
0980 void KolfGame::keyReleaseEvent(QKeyEvent *e)
0981 {
0982     if (e->isAutoRepeat() || m_ignoreEvents)
0983         return;
0984 
0985     if (e->key() == Qt::Key_Space || e->key() == Qt::Key_Down)
0986         puttRelease();
0987     else if ((e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete) && !(e->modifiers() & Qt::ControlModifier))
0988     {
0989         if (editing && selectedItem)
0990         {
0991             CanvasItem *citem = dynamic_cast<CanvasItem *>(selectedItem);
0992             if (!citem)
0993                 return;
0994             QGraphicsItem *item = dynamic_cast<QGraphicsItem *>(citem);
0995             if (citem && !dynamic_cast<Ball*>(item))
0996             {
0997                 lastDelId = citem->curId();
0998 
0999                 m_topLevelQItems.removeAll(item);
1000                 m_moveableQItems.removeAll(item);
1001                 delete citem;
1002                 setSelectedItem(nullptr);
1003 
1004                 setModified(true);
1005             }
1006         }
1007     }
1008     else if (e->key() == Qt::Key_I || e->key() == Qt::Key_Up)
1009         toggleShowInfo();
1010 }
1011 
1012 void KolfGame::resizeEvent( QResizeEvent* ev )
1013 {
1014     int newW = ev->size().width();
1015     int newH = ev->size().height();
1016     int oldW = ev->oldSize().width();
1017     int oldH = ev->oldSize().height();
1018 
1019     if(oldW<=0 || oldH<=0) //this is the first draw so no point wasting resources resizing yet
1020         return;
1021     else if( (oldW==newW) && (oldH==newH) )
1022         return;
1023 
1024     int setSize = qMin(newW, newH);
1025     QGraphicsView::resize(setSize, setSize); //make sure new size is square
1026 }
1027 
1028 void KolfGame::puttRelease()
1029 {
1030     if (!m_useAdvancedPutting && putting && !editing)
1031     {
1032         putting = false;
1033         stroking = true;
1034     }
1035 }
1036 
1037 void KolfGame::stoppedBall()
1038 {
1039     if (!inPlay)
1040     {
1041         inPlay = true;
1042         dontAddStroke = true;
1043     }
1044 }
1045 
1046 void KolfGame::timeout()
1047 {
1048     Ball *curBall = (*curPlayer).ball();
1049 
1050     // test if the ball is gone
1051     // in this case we want to stop the ball and
1052     // later undo the shot
1053     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1054     {
1055                 //QGV handles management of dirtied rects for us
1056         //course->update();
1057 
1058         if (!QRectF(QPointF(), courseBoard->logicalSize()).contains((*it).ball()->pos()))
1059         {
1060             (*it).ball()->setState(Stopped);
1061 
1062             // don't do it if he's past maxStrokes
1063             if ((*it).score(curHole) < holeInfo.maxStrokes() - 1 || !holeInfo.hasMaxStrokes())
1064             {
1065                 loadStateList();
1066             }
1067             shotDone();
1068 
1069             return;
1070         }
1071     }
1072 
1073     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1074         if ((*it).ball()->forceStillGoing() || ((*it).ball()->curState() == Rolling && Vector((*it).ball()->velocity()).magnitude() > 0 && (*it).ball()->isVisible()))
1075             return;
1076 
1077     int curState = curBall->curState();
1078     if (curState == Stopped && inPlay)
1079     {
1080         inPlay = false;
1081         QTimer::singleShot(0, this, &KolfGame::shotDone);
1082     }
1083 
1084     if (curState == Holed && inPlay)
1085     {
1086         Q_EMIT inPlayEnd();
1087 
1088         int curScore = (*curPlayer).score(curHole);
1089         if (!dontAddStroke)
1090             curScore++;
1091 
1092         if (curScore == 1)
1093         {
1094             playSound(Sound::HoleINone);
1095         }
1096         else if (curScore <= holeInfo.par())
1097         {
1098             playSound(Sound::WooHoo);
1099         }
1100 
1101         (*curPlayer).ball()->setZValue((*curPlayer).ball()->zValue() + .1 - (.1)/(curScore));
1102 
1103         if (allPlayersDone())
1104         {
1105             inPlay = false;
1106 
1107             if (curHole > 0 && !dontAddStroke)
1108             {
1109                 (*curPlayer).addStrokeToHole(curHole);
1110                 Q_EMIT scoreChanged((*curPlayer).id(), curHole, (*curPlayer).score(curHole));
1111             }
1112             QTimer::singleShot(600, this, &KolfGame::holeDone);
1113         }
1114         else
1115         {
1116             inPlay = false;
1117             QTimer::singleShot(0, this, &KolfGame::shotDone);
1118         }
1119     }
1120 }
1121 
1122 void KolfGame::fastTimeout()
1123 {
1124     // do regular advance every other time
1125     if (regAdv)
1126         course->advance();
1127     regAdv = !regAdv;
1128 
1129     if (editing)
1130         return;
1131 
1132     // do Box2D advance
1133     //Because there are so much CanvasItems out there, there is currently no
1134     //easy and/or systematic approach to iterate over all of them, except for
1135     //using the b2Bodies available on the world.
1136 
1137     //prepare simulation
1138     for (b2Body* body = g_world->GetBodyList(); body; body = body->GetNext())
1139     {
1140         CanvasItem* citem = static_cast<CanvasItem*>(body->GetUserData());
1141         if (citem)
1142         {
1143             citem->startSimulation();
1144             //HACK: the following should not be necessary at this point
1145             QGraphicsItem* qitem = dynamic_cast<QGraphicsItem*>(citem);
1146             if (qitem)
1147                 citem->updateZ(qitem);
1148         }
1149     }
1150     //step world
1151     //NOTE: I previously set timeStep to 1.0 so that CItem's velocity()
1152     //corresponds to the position change per step. In this case, the
1153     //velocity would be scaled by Kolf::Box2DScaleFactor, which would result in
1154     //very small velocities (below Box2D's internal cutoff thresholds!) for
1155     //usual movements. Therefore, we apply the scaling to the timestep instead.
1156     const double timeStep = 1.0 * Kolf::Box2DScaleFactor;
1157     g_world->Step(timeStep, 10, 10); //parameters 2/3 = iteration counts (TODO: optimize)
1158     //conclude simulation
1159     for (b2Body* body = g_world->GetBodyList(); body; body = body->GetNext())
1160     {
1161         CanvasItem* citem = static_cast<CanvasItem*>(body->GetUserData());
1162         if (citem)
1163         {
1164             citem->endSimulation();
1165         }
1166     }
1167 }
1168 
1169 void KolfGame::ballMoved()
1170 {
1171     if (putter->isVisible())
1172     {
1173         putter->setPos((*curPlayer).ball()->x(), (*curPlayer).ball()->y());
1174         updateMouse();
1175     }
1176 }
1177 
1178 void KolfGame::putterTimeout()
1179 {
1180     if (inPlay || editing)
1181         return;
1182 
1183     if (m_useAdvancedPutting)
1184     {
1185         if (putting)
1186         {
1187             const qreal base = 2.0;
1188 
1189             if (puttReverse && strength <= 0)
1190             {
1191                 // aborted
1192                 putting = false;
1193                 strokeCircle->setVisible(false); 
1194             }
1195             else if (strength > maxStrength || puttReverse)
1196             {
1197                 // decreasing strength as we've reached the top
1198                 puttReverse = true;
1199                 strength -= pow(base, qreal(strength / maxStrength)) - 1.8;
1200                 if ((int)strength < puttCount * 2)
1201                 {
1202                     puttCount--;
1203                     if (puttCount >= 0)
1204                         putter->go(Forwards);
1205                 }
1206             }
1207             else
1208             {
1209                 // make the increase at high strength faster
1210                 strength += pow(base, strength / maxStrength) - .3;
1211                 if ((int)strength > puttCount * 2)
1212                 {
1213                     putter->go(Backwards);
1214                     puttCount++;
1215                 }
1216             }
1217             // make the visible steps at high strength smaller
1218             strokeCircle->setValue(pow(strength / maxStrength, 0.8) * 360); 
1219         }
1220         else if (stroking)
1221         {
1222             double al = strokeCircle->value(); 
1223             if (al >= 45)
1224                 al -= 0.2 + strength / 50 + al / 100;
1225             else
1226                 al -= 0.2 + strength / 50;
1227 
1228             if (puttReverse)
1229             {
1230                 // show the stroke
1231                 puttCount--;
1232                 if (puttCount >= 0)
1233                     putter->go(Forwards);
1234                 else
1235                 {
1236                     strokeCircle->setVisible(false);
1237                     finishStroking = false;
1238                     putterTimer->stop();
1239                     putting = false;
1240                     stroking = false;
1241                     shotStart();
1242                 }
1243             }
1244             else if (al < -45 || finishStroking)
1245             {
1246                 strokeCircle->setValue(al); 
1247                 int deg;
1248                         auto *generator = QRandomGenerator::global();
1249                 // if > 45 or < -45 then bad stroke
1250                 if (al > 45)
1251                 {
1252                     deg = putter->curDeg() - 45 + rand() % 90;
1253                             strength -= generator->bounded((int)strength);
1254                 }
1255                 else if (!finishStroking)
1256                 {
1257                     deg = putter->curDeg() - 45 + rand() % 90;
1258                             strength -= generator->bounded((int)strength);
1259                 }
1260                 else
1261                     deg = putter->curDeg() + (int)(strokeCircle->value() / 3);
1262 
1263                 if (deg < 0)
1264                     deg += 360;
1265                 else if (deg > 360)
1266                     deg -= 360;
1267 
1268                 putter->setDeg(deg);
1269                 puttReverse = true;
1270             }
1271             else
1272             {
1273                 strokeCircle->setValue(al);
1274                 putterTimer->start(putterTimerMsec/10);
1275             }
1276         }
1277     }
1278     else
1279     {
1280         if (putting)
1281         {
1282             putter->go(Backwards);
1283             puttCount++;
1284             strength += 1.5;
1285             if (strength > maxStrength)
1286             {
1287                 putting = false;
1288                 stroking = true;
1289             }
1290         }
1291         else if (stroking)
1292         {
1293             if (putter->curLen() < (*curPlayer).ball()->height() + 2)
1294             {
1295                 stroking = false;
1296                 putterTimer->stop();
1297                 putting = false;
1298                 stroking = false;
1299                 shotStart();
1300             }
1301 
1302             putter->go(Forwards);
1303             putterTimer->start(putterTimerMsec/10);
1304         }
1305     }
1306 }
1307 
1308 void KolfGame::autoSaveTimeout()
1309 {
1310     // this should be a config option
1311     // until it is i'll disable it
1312     if (editing)
1313     {
1314         //save();
1315     }
1316 }
1317 
1318 void KolfGame::recreateStateList()
1319 {
1320     savedState.clear();
1321     for (QGraphicsItem* item : std::as_const(m_topLevelQItems)) {
1322         if (dynamic_cast<Ball*>(item)) continue; //see below
1323         CanvasItem* citem = dynamic_cast<CanvasItem*>(item);
1324         if (citem)
1325         {
1326             const QString key = makeStateGroup(citem->curId(), citem->name());
1327             savedState.insert(key, item->pos());
1328         }
1329     }
1330 
1331     ballStateList.clear();
1332     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1333         ballStateList.append((*it).stateInfo(curHole));
1334 
1335     ballStateList.canUndo = true;
1336 }
1337 
1338 void KolfGame::undoShot()
1339 {
1340     if (ballStateList.canUndo)
1341         loadStateList();
1342 }
1343 
1344 void KolfGame::loadStateList()
1345 {
1346     for (QGraphicsItem* item : std::as_const(m_topLevelQItems)) {
1347         if (dynamic_cast<Ball*>(item)) continue; //see below
1348         CanvasItem* citem = dynamic_cast<CanvasItem*>(item);
1349         if (citem)
1350         {
1351             const QString key = makeStateGroup(citem->curId(), citem->name());
1352             const QPointF currentPos = item->pos();
1353             const QPointF posDiff = savedState.value(key, currentPos) - currentPos;
1354             citem->moveBy(posDiff.x(), posDiff.y());
1355         }
1356     }
1357 
1358     for (BallStateList::Iterator it = ballStateList.begin(); it != ballStateList.end(); ++it)
1359     {
1360         BallStateInfo info = (*it);
1361         Player &player = (*(players->begin() + (info.id - 1) ));
1362         player.ball()->setPos(info.spot.x(), info.spot.y());
1363         player.ball()->setBeginningOfHole(info.beginningOfHole);
1364         if ((*curPlayer).id() == info.id)
1365             ballMoved();
1366         else
1367             player.ball()->setVisible(!info.beginningOfHole);
1368         player.setScoreForHole(info.score, curHole);
1369         player.ball()->setState(info.state);
1370         Q_EMIT scoreChanged(info.id, curHole, info.score);
1371     }
1372 }
1373 
1374 void KolfGame::shotDone()
1375 {
1376     inPlay = false;
1377     Q_EMIT inPlayEnd();
1378     setFocus();
1379 
1380     Ball *ball = (*curPlayer).ball();
1381 
1382     if(ball->curState() == Rolling) {
1383         // This is a bit of a hack, since we have different timers for detecting shotDone and for doing animation, it can happen that at some point we think the shot
1384         // was done, do a singleshot 0 call, but then we continue the animation and we realize it's not really done, so here make sure we're realy done
1385         // before adding a stroke to the player
1386         inPlay = true;
1387         return;
1388     }
1389 
1390     if (!dontAddStroke && (*curPlayer).numHoles())
1391         (*curPlayer).addStrokeToHole(curHole);
1392 
1393     dontAddStroke = false;
1394 
1395     // do hack stuff, shouldn't be done here
1396 
1397     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1398     {
1399         if ((*it).ball()->addStroke())
1400         {
1401             for (int i = 1; i <= (*it).ball()->addStroke(); ++i)
1402                 (*it).addStrokeToHole(curHole);
1403 
1404             // emit that we have a new stroke count
1405             Q_EMIT scoreChanged((*it).id(), curHole, (*it).score(curHole));
1406         }
1407         (*it).ball()->setAddStroke(0);
1408     }
1409 
1410     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1411     {
1412         Ball *ball = (*it).ball();
1413 
1414         if (ball->curState() == Holed)
1415             continue;
1416 
1417         Vector oldVelocity;
1418         if (ball->placeOnGround(oldVelocity))
1419         {
1420             ball->setPlaceOnGround(false);
1421 
1422             QStringList options;
1423             const QString placeOutside = i18n("Drop Outside of Hazard");
1424             const QString rehit = i18n("Rehit From Last Location");
1425             options << placeOutside << rehit;
1426             const QString choice = KComboBoxDialog::getItem(i18n("What would you like to do for your next shot?"), i18n("%1 is in a Hazard", (*it).name()), options, placeOutside, QStringLiteral("hazardOptions"));
1427 
1428             if (choice == placeOutside)
1429             {
1430                 (*it).ball()->setDoDetect(false);
1431 
1432                 QPointF pos = ball->pos();
1433                 //normalize old velocity
1434                 const QPointF v = oldVelocity / oldVelocity.magnitude();
1435 
1436                 while (1)
1437                 {
1438                     QList<QGraphicsItem *> list = ball->collidingItems();
1439                     bool keepMoving = false;
1440                     while (!list.isEmpty())
1441                     {
1442                         QGraphicsItem *item = list.takeFirst();
1443                         if (item->data(0) == Rtti_DontPlaceOn)
1444                             keepMoving = true;
1445                     }
1446                     if (!keepMoving)
1447                         break;
1448 
1449                     const qreal movePixel = 3.0;
1450                     pos -= v * movePixel;
1451                     ball->setPos(pos);
1452                 }
1453             }
1454             else if (choice == rehit)
1455             {
1456                 for (BallStateList::Iterator it = ballStateList.begin(); it != ballStateList.end(); ++it)
1457                 {
1458                     if ((*it).id == (*curPlayer).id())
1459                     {
1460                         if ((*it).beginningOfHole)
1461                             ball->setPos(whiteBall->x(), whiteBall->y());
1462                         else
1463                             ball->setPos((*it).spot.x(), (*it).spot.y());
1464 
1465                         break;
1466                     }
1467                 }
1468             }
1469 
1470             ball->setVisible(true);
1471             ball->setState(Stopped); 
1472 
1473             (*it).ball()->setDoDetect(true);
1474             ball->collisionDetect();
1475         }
1476     }
1477 
1478     // emit again
1479     Q_EMIT scoreChanged((*curPlayer).id(), curHole, (*curPlayer).score(curHole));
1480 
1481     if(ball->curState() == Rolling) {
1482         inPlay = true; 
1483         return;
1484     }
1485 
1486     ball->setVelocity(Vector());
1487 
1488     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1489     {
1490         Ball *ball = (*it).ball();
1491 
1492         int curStrokes = (*it).score(curHole);
1493         if (curStrokes >= holeInfo.maxStrokes() && holeInfo.hasMaxStrokes())
1494         {
1495             ball->setState(Holed);
1496             ball->setVisible(false);
1497 
1498             // move to center in case he/she hit out
1499             ball->setPos(width / 2, height / 2);
1500             playerWhoMaxed = (*it).name();
1501 
1502             if (allPlayersDone())
1503             {
1504                 startNextHole();
1505                 QTimer::singleShot(100, this, &KolfGame::emitMax);
1506                 return;
1507             }
1508 
1509             QTimer::singleShot(100, this, &KolfGame::emitMax);
1510         }
1511     }
1512 
1513     // change player to next player
1514     // skip player if he's Holed
1515     do
1516     {
1517         curPlayer++;
1518         if (curPlayer == players->end())
1519             curPlayer = players->begin();
1520     }
1521     while ((*curPlayer).ball()->curState() == Holed);
1522 
1523     Q_EMIT newPlayersTurn(&(*curPlayer));
1524 
1525     (*curPlayer).ball()->setVisible(true);
1526 
1527     inPlay = false;
1528     (*curPlayer).ball()->collisionDetect();
1529 
1530     putter->setAngle((*curPlayer).ball());
1531     putter->setOrigin((*curPlayer).ball()->x(), (*curPlayer).ball()->y());
1532     updateMouse();
1533 }
1534 
1535 void KolfGame::emitMax()
1536 {
1537     Q_EMIT maxStrokesReached(playerWhoMaxed);
1538 }
1539 
1540 void KolfGame::startBall(const Vector &velocity)
1541 {
1542     playSound(Sound::Hit);
1543     Q_EMIT inPlayStart();
1544     putter->setVisible(false);
1545 
1546     (*curPlayer).ball()->setState(Rolling);
1547     (*curPlayer).ball()->setVelocity(velocity);
1548     (*curPlayer).ball()->shotStarted();
1549 
1550     for (QGraphicsItem* qitem : std::as_const(m_topLevelQItems)) {
1551         CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
1552         if (citem)
1553             citem->shotStarted();
1554     }
1555 
1556     inPlay = true;
1557 }
1558 
1559 void KolfGame::shotStart()
1560 {
1561     // ensure we never hit the ball back into the hole which
1562     // can cause hole skippage
1563     if ((*curPlayer).ball()->curState() == Holed)
1564         return;
1565 
1566     // save state
1567     recreateStateList();
1568 
1569     putter->saveAngle((*curPlayer).ball());
1570     strength /= 8;
1571     if (!strength)
1572         strength = 1;
1573 
1574     //kDebug(12007) << "Start started. BallX:" << (*curPlayer).ball()->x() << ", BallY:" << (*curPlayer).ball()->y() << ", Putter Angle:" << putter->curAngle() << ", Vector Strength: " << strength;
1575 
1576     (*curPlayer).ball()->collisionDetect();
1577 
1578     startBall(Vector::fromMagnitudeDirection(strength, -(putter->curAngle() + M_PI)));
1579 
1580     addHoleInfo(ballStateList);
1581 }
1582 
1583 void KolfGame::addHoleInfo(BallStateList &list)
1584 {
1585     list.player = (*curPlayer).id();
1586     list.vector = (*curPlayer).ball()->velocity();
1587     list.hole = curHole;
1588 }
1589 
1590 void KolfGame::sayWhosGoing()
1591 {
1592     if (players->count() >= 2)
1593     {
1594         KMessageBox::information(this, i18n("%1 will start off.", (*curPlayer).name()), i18n("New Hole"), QStringLiteral("newHole"));
1595     }
1596 }
1597 
1598 void KolfGame::holeDone()
1599 {
1600     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1601         (*it).ball()->setVisible(false);
1602     startNextHole();
1603     sayWhosGoing();
1604 }
1605 
1606 // this function is WAY too smart for it's own good
1607 // ie, bad design :-(
1608 void KolfGame::startNextHole()
1609 {
1610     setFocus();
1611 
1612     bool reset = true;
1613     if (askSave(true))
1614     {
1615         if (allPlayersDone())
1616         {
1617             // we'll reload this hole, but not reset
1618             curHole--;
1619             reset = false;
1620         }
1621         else
1622             return;
1623     }
1624     else
1625         setModified(false);
1626 
1627     pause();
1628 
1629     dontAddStroke = false;
1630 
1631     inPlay = false;
1632     timer->stop();
1633     putter->resetAngles();
1634 
1635     int oldCurHole = curHole;
1636     curHole++;
1637     Q_EMIT currentHole(curHole);
1638 
1639     if (reset)
1640     {
1641         whiteBall->setPos(width/2, height/2);
1642         holeInfo.borderWallsChanged(true);
1643     }
1644 
1645     int leastScore = INT_MAX;
1646 
1647     // to get the first player to go first on every hole,
1648     // don't do the score stuff below
1649     curPlayer = players->begin();
1650 
1651     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1652     {
1653         if (curHole > 1)
1654         {
1655             bool ahead = false;
1656             if ((*it).lastScore() != 0)
1657             {
1658                 if ((*it).lastScore() < leastScore)
1659                     ahead = true;
1660                 else if ((*it).lastScore() == leastScore)
1661                 {
1662                     for (int i = curHole - 1; i > 0; --i)
1663                     {
1664                         while(i > (*it).scores().size())
1665                             i--;
1666 
1667                         const int thisScore = (*it).score(i);
1668                         const int thatScore = (*curPlayer).score(i);
1669                         if (thisScore < thatScore)
1670                         {
1671                             ahead = true;
1672                             break;
1673                         }
1674                         else if (thisScore > thatScore)
1675                             break;
1676                     }
1677                 }
1678             }
1679 
1680             if (ahead)
1681             {
1682                 curPlayer = it;
1683                 leastScore = (*it).lastScore();
1684             }
1685         }
1686 
1687         if (reset)
1688             (*it).ball()->setPos(width / 2, height / 2);
1689         else
1690             (*it).ball()->setPos(whiteBall->x(), whiteBall->y());
1691 
1692         (*it).ball()->setState(Stopped);
1693 
1694         // this gets set to false when the ball starts
1695         // to move by the Mr. Ball himself.
1696         (*it).ball()->setBeginningOfHole(true);
1697         if ((int)(*it).scores().count() < curHole)
1698             (*it).addHole();
1699         (*it).ball()->setVelocity(Vector());
1700         (*it).ball()->setVisible(false);
1701     }
1702 
1703     Q_EMIT newPlayersTurn(&(*curPlayer));
1704 
1705     if (reset)
1706         openFile();
1707 
1708     inPlay = false;
1709     timer->start(timerMsec);
1710 
1711     if(size().width()!=400 || size().height()!=400) { //not default size, so resizing needed
1712         int setSize = qMin(size().width(), size().height());
1713         //resize needs to be called for setSize+1 first because otherwise it doesn't seem to get called (not sure why)
1714         QGraphicsView::resize(setSize+1, setSize+1);
1715         QGraphicsView::resize(setSize, setSize);
1716     }
1717 
1718     // if (false) { we're done with the round! }
1719     if (oldCurHole != curHole)
1720     {
1721         for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it) {
1722             (*it).ball()->setPlaceOnGround(false);
1723             while( (*it).numHoles() < (unsigned)curHole)
1724                 (*it).addHole();
1725         }
1726 
1727         // here we have to make sure the scoreboard shows
1728         // all of the holes up until now;
1729 
1730         for (; scoreboardHoles < curHole; ++scoreboardHoles)
1731         {
1732             cfgGroup = KConfigGroup(cfg->group(QStringLiteral("%1-hole@-50,-50|0").arg(scoreboardHoles + 1)));
1733             Q_EMIT newHole(cfgGroup.readEntry("par", 3));
1734         }
1735 
1736         resetHoleScores();
1737         updateShowInfo();
1738 
1739         // this is from shotDone()
1740         (*curPlayer).ball()->setVisible(true);
1741         putter->setOrigin((*curPlayer).ball()->x(), (*curPlayer).ball()->y());
1742         updateMouse();
1743 
1744         ballStateList.canUndo = false;
1745 
1746         (*curPlayer).ball()->collisionDetect();
1747     }
1748 
1749     unPause();
1750 }
1751 
1752 void KolfGame::showInfoDlg(bool addDontShowAgain)
1753 {
1754     KMessageBox::information(parentWidget(),
1755             i18n("Course name: %1", holeInfo.name()) + QStringLiteral("\n")
1756             + i18n("Created by %1", holeInfo.author()) + QStringLiteral("\n")
1757             + i18np("%1 hole", "%1 holes", highestHole),
1758             i18nc("@title:window", "Course Information"),
1759             addDontShowAgain? holeInfo.name() + QStringLiteral(" ") + holeInfo.author() : QString());
1760 }
1761 
1762 void KolfGame::openFile()
1763 {
1764     QList<QGraphicsItem*> newTopLevelQItems;
1765     const auto currentTopLevelQItems = m_topLevelQItems;
1766     for (QGraphicsItem* qitem : currentTopLevelQItems) {
1767         if (dynamic_cast<Ball*>(qitem))
1768         {
1769             //do not delete balls
1770             newTopLevelQItems << qitem;
1771             continue;
1772         }
1773         CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
1774         if (citem)
1775         {
1776             delete citem;
1777         }
1778     }
1779 
1780     m_moveableQItems = m_topLevelQItems = newTopLevelQItems;
1781     selectedItem = nullptr;
1782 
1783     // will tell basic course info
1784     // we do this here for the hell of it.
1785     // there is no fake id, by the way,
1786     // because it's old and when i added ids i forgot to change it.
1787     cfgGroup = KConfigGroup(cfg->group(QStringLiteral("0-course@-50,-50")));
1788     holeInfo.setAuthor(cfgGroup.readEntry("author", holeInfo.author()));
1789     holeInfo.setName(cfgGroup.readEntry("Name", holeInfo.name()));
1790     holeInfo.setUntranslatedName(cfgGroup.readEntryUntranslated("Name", holeInfo.untranslatedName()));
1791     Q_EMIT titleChanged(holeInfo.name());
1792 
1793     cfgGroup = KConfigGroup(KSharedConfig::openConfig(filename), QStringLiteral("%1-hole@-50,-50|0").arg(curHole));
1794     curPar = cfgGroup.readEntry("par", 3);
1795     holeInfo.setPar(curPar);
1796     holeInfo.borderWallsChanged(cfgGroup.readEntry("borderWalls", holeInfo.borderWalls()));
1797     holeInfo.setMaxStrokes(cfgGroup.readEntry("maxstrokes", 10));
1798 
1799     QStringList missingPlugins;
1800 
1801     // The "for" loop depends on the list of groups being in sorted order.
1802     QStringList groups = cfg->groupList();
1803     groups.sort();
1804 
1805     int numItems = 0;
1806     int _highestHole = 0;
1807 
1808     for (QStringList::const_iterator it = groups.constBegin(); it != groups.constEnd(); ++it)
1809     {
1810         // Format of group name is [<holeNum>-<name>@<x>,<y>|<id>]
1811         cfgGroup = KConfigGroup(cfg->group(*it));
1812 
1813         const int len = (*it).length();
1814         const int dashIndex = (*it).indexOf(QLatin1Char('-'));
1815         const int holeNum = QStringView(*it).left(dashIndex).toInt();
1816         if (holeNum > _highestHole)
1817             _highestHole = holeNum;
1818 
1819         const int atIndex = (*it).indexOf(QLatin1Char('@'));
1820         const QString name = (*it).mid(dashIndex + 1, atIndex - (dashIndex + 1));
1821 
1822         if (holeNum != curHole)
1823         {
1824             // Break before reading all groups, if the highest hole
1825             // number is known and all items in curHole are done.
1826             if (numItems && !recalcHighestHole)
1827                 break;
1828             continue;
1829         }
1830         numItems++;
1831 
1832 
1833         const int commaIndex = (*it).indexOf(QLatin1Char(','));
1834         const int pipeIndex = (*it).indexOf(QLatin1Char('|'));
1835         const int x = QStringView(*it).mid(atIndex + 1, commaIndex - (atIndex + 1)).toInt();
1836         const int y = QStringView(*it).mid(commaIndex + 1, pipeIndex - (commaIndex + 1)).toInt();
1837         // will tell where ball is
1838         if (name == QLatin1String("ball"))
1839         {
1840             for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1841                 (*it).ball()->setPos(x, y);
1842             whiteBall->setPos(x, y);
1843             continue;
1844         }
1845         const int id = QStringView(*it).right(len - (pipeIndex + 1)).toInt();
1846         QGraphicsItem* newItem = m_factory.createInstance(name, courseBoard, g_world);
1847         if (newItem)
1848         {
1849             m_topLevelQItems << newItem;
1850             m_moveableQItems << newItem;
1851             CanvasItem *sceneItem = dynamic_cast<CanvasItem *>(newItem);
1852 
1853             if (!sceneItem)
1854                 continue;
1855 
1856             sceneItem->setId(id);
1857             sceneItem->setGame(this);
1858             sceneItem->editModeChanged(editing);
1859             sceneItem->setName(name);
1860             m_moveableQItems.append(sceneItem->moveableItems());
1861 
1862             sceneItem->setPosition(QPointF(x, y));
1863             newItem->setVisible(true);
1864 
1865             // make things actually show
1866             cfgGroup = KConfigGroup(cfg->group(makeGroup(id, curHole, sceneItem->name(), x, y)));
1867             sceneItem->load(&cfgGroup);
1868         }
1869         else if (name != QLatin1String("hole") && !missingPlugins.contains(name))
1870             missingPlugins.append(name);
1871 
1872     }
1873 
1874     if (!missingPlugins.empty())
1875     {
1876         KMessageBox::informationList(this, QStringLiteral("<p>") + i18n("This hole uses the following plugins, which you do not have installed:") + QStringLiteral("</p>"), missingPlugins, QString(), QStringLiteral("%1 warning").arg(holeInfo.untranslatedName() + QString::number(curHole)));
1877     }
1878 
1879     lastDelId = -1;
1880 
1881     // if it's the first hole let's not
1882     if (!numItems && curHole > 1 && !addingNewHole && curHole >= _highestHole)
1883     {
1884         // we're done, let's quit
1885         curHole--;
1886         pause();
1887         Q_EMIT holesDone();
1888 
1889         // tidy things up
1890         setBorderWalls(false);
1891         clearHole();
1892         setModified(false);
1893         for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1894             (*it).ball()->setVisible(false);
1895 
1896         return;
1897     }
1898 
1899     // do it down here; if !hasFinalLoad, do it up there!
1900     //QGraphicsItem *qsceneItem = 0;
1901     QList<QGraphicsItem *>::const_iterator qsceneItem;
1902     QList<CanvasItem *> todo;
1903     QList<QGraphicsItem *> qtodo;
1904 
1905     if (curHole > _highestHole)
1906         _highestHole = curHole;
1907 
1908     if (recalcHighestHole)
1909     {
1910         highestHole = _highestHole;
1911         recalcHighestHole = false;
1912         Q_EMIT largestHole(highestHole);
1913     }
1914 
1915     if (curHole == 1 && !filename.isNull() && !infoShown)
1916     {
1917         // let's not now, because they see it when they choose course
1918         //showInfoDlg(true);
1919         infoShown = true;
1920     }
1921 
1922     setModified(false);
1923 }
1924 
1925 void KolfGame::addNewObject(const QString& identifier)
1926 {
1927     QGraphicsItem *newItem = m_factory.createInstance(identifier, courseBoard, g_world);
1928 
1929     m_topLevelQItems << newItem;
1930     m_moveableQItems << newItem;
1931     if(!newItem->isVisible())
1932         newItem->setVisible(true);
1933 
1934     CanvasItem *sceneItem = dynamic_cast<CanvasItem *>(newItem);
1935     if (!sceneItem)
1936         return;
1937 
1938     // we need to find a number that isn't taken
1939     int i = lastDelId > 0? lastDelId : m_topLevelQItems.count() - 30;
1940     if (i <= 0)
1941         i = 0;
1942 
1943     for (;; ++i)
1944     {
1945         bool found = false;
1946         for (QGraphicsItem* qitem : std::as_const(m_topLevelQItems)) {
1947             CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
1948             if (citem)
1949             {
1950                 if (citem->curId() == i)
1951                 {
1952                     found = true;
1953                     break;
1954                 }
1955             }
1956         }
1957 
1958 
1959         if (!found)
1960             break;
1961     }
1962     sceneItem->setId(i);
1963 
1964     sceneItem->setGame(this);
1965 
1966     const auto infoItems = sceneItem->infoItems();
1967     for (QGraphicsItem* qitem : infoItems)
1968         qitem->setVisible(m_showInfo);
1969 
1970     sceneItem->editModeChanged(editing);
1971 
1972     sceneItem->setName(identifier);
1973     m_moveableQItems.append(sceneItem->moveableItems());
1974 
1975     newItem->setPos(width/2 - 18, height / 2 - 18);
1976     sceneItem->moveBy(0, 0);
1977     sceneItem->setSize(newItem->boundingRect().size());
1978 
1979     setModified(true);
1980 }
1981 
1982 bool KolfGame::askSave(bool noMoreChances)
1983 {
1984     if (!modified)
1985         // not cancel, don't save
1986         return false;
1987 
1988     int result = KMessageBox::warningTwoActionsCancel(this,
1989                              i18n("There are unsaved changes to current hole. Save them?"),
1990                              i18n("Unsaved Changes"),
1991                              KStandardGuiItem::save(),
1992                              noMoreChances? KStandardGuiItem::discard() : KGuiItem(i18n("Save &Later")),
1993                              KStandardGuiItem::cancel(),
1994                              noMoreChances? QStringLiteral("DiscardAsk") : QStringLiteral("SaveAsk"));
1995     switch (result)
1996     {
1997         case KMessageBox::PrimaryAction:
1998             save();
1999             [[fallthrough]];
2000 
2001         case KMessageBox::SecondaryAction:
2002             return false;
2003             break;
2004 
2005         case KMessageBox::Cancel:
2006             return true;
2007             break;
2008 
2009         default:
2010             break;
2011     }
2012 
2013     return false;
2014 }
2015 
2016 void KolfGame::addNewHole()
2017 {
2018     if (askSave(true))
2019         return;
2020 
2021     // either it's already false
2022     // because it was saved by askSave(),
2023     // or the user pressed the 'discard' button
2024     setModified(false);
2025 
2026     // find highest hole num, and create new hole
2027     // now openFile makes highest hole for us
2028 
2029     addingNewHole = true;
2030     curHole = highestHole;
2031     recalcHighestHole = true;
2032     startNextHole();
2033     addingNewHole = false;
2034     Q_EMIT currentHole(curHole);
2035 
2036     // make sure even the current player isn't showing
2037     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
2038         (*it).ball()->setVisible(false);
2039 
2040     whiteBall->setVisible(editing);
2041     putter->setVisible(!editing);
2042     inPlay = false;
2043 
2044     // add default objects
2045     const auto knownTypes = m_factory.knownTypes();
2046     for (const Kolf::ItemMetadata& metadata : knownTypes)
2047         if (metadata.addOnNewHole)
2048             addNewObject(metadata.identifier);
2049 
2050     save();
2051 }
2052 
2053 // kantan deshou ;-)
2054 void KolfGame::resetHole()
2055 {
2056     if (askSave(true))
2057         return;
2058     setModified(false);
2059     curHole--;
2060     startNextHole();
2061     resetHoleScores();
2062 }
2063 
2064 void KolfGame::resetHoleScores()
2065 {
2066     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
2067     {
2068         (*it).resetScore(curHole);
2069         Q_EMIT scoreChanged((*it).id(), curHole, 0);
2070     }
2071 }
2072 
2073 void KolfGame::clearHole()
2074 {
2075     QList<QGraphicsItem*> newTopLevelQItems;
2076     const auto currentTopLevelQItems = m_topLevelQItems;
2077     for (QGraphicsItem* qitem : currentTopLevelQItems) {
2078         if (dynamic_cast<Ball*>(qitem))
2079         {
2080             //do not delete balls
2081             newTopLevelQItems << qitem;
2082             continue;
2083         }
2084         CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
2085         if (citem)
2086         {
2087             delete citem;
2088         }
2089     }
2090 
2091     m_moveableQItems = m_topLevelQItems = newTopLevelQItems;
2092     setSelectedItem(nullptr);
2093 
2094     // add default objects
2095     const auto knownTypes = m_factory.knownTypes();
2096     for (const Kolf::ItemMetadata& metadata : knownTypes)
2097         if (metadata.addOnNewHole)
2098             addNewObject(metadata.identifier);
2099 
2100     setModified(true);
2101 }
2102 
2103 void KolfGame::switchHole(int hole)
2104 {
2105     if (inPlay)
2106         return;
2107     if (hole < 1 || hole > highestHole)
2108         return;
2109 
2110     bool wasEditing = editing;
2111     if (editing)
2112         toggleEditMode();
2113 
2114     if (askSave(true))
2115         return;
2116     setModified(false);
2117 
2118     curHole = hole;
2119     resetHole();
2120 
2121     if (wasEditing)
2122         toggleEditMode();
2123 }
2124 
2125 void KolfGame::switchHole(const QString &holestring)
2126 {
2127     bool ok;
2128     int hole = holestring.toInt(&ok);
2129     if (!ok)
2130         return;
2131     switchHole(hole);
2132 }
2133 
2134 void KolfGame::nextHole()
2135 {
2136     switchHole(curHole + 1);
2137 }
2138 
2139 void KolfGame::prevHole()
2140 {
2141     switchHole(curHole - 1);
2142 }
2143 
2144 void KolfGame::firstHole()
2145 {
2146     switchHole(1);
2147 }
2148 
2149 void KolfGame::lastHole()
2150 {
2151     switchHole(highestHole);
2152 }
2153 
2154 void KolfGame::randHole()
2155 {
2156     const int newHole = QRandomGenerator::global()->bounded(1, highestHole);
2157     switchHole(newHole);
2158 }
2159 
2160 void KolfGame::save()
2161 {
2162     if (filename.isEmpty())
2163     {
2164         QPointer<QFileDialog> fileSaveDialog = new QFileDialog(this);
2165         fileSaveDialog->setWindowTitle(i18nc("@title:window", "Pick Kolf Course to Save To"));
2166         fileSaveDialog->setMimeTypeFilters(QStringList(QStringLiteral("application/x-kourse")));
2167         fileSaveDialog->setAcceptMode(QFileDialog::AcceptSave);
2168         if (fileSaveDialog->exec() == QDialog::Accepted) {
2169             QUrl newfile = fileSaveDialog->selectedUrls().first();
2170             if (newfile.isEmpty()) {
2171                 return;
2172             }
2173             else {
2174                 setFilename(newfile.toLocalFile());
2175             }
2176         }
2177         delete fileSaveDialog;
2178     }
2179 
2180     Q_EMIT parChanged(curHole, holeInfo.par());
2181     Q_EMIT titleChanged(holeInfo.name());
2182 
2183     const QStringList groups = cfg->groupList();
2184 
2185     // wipe out all groups from this hole
2186     for (QStringList::const_iterator it = groups.begin(); it != groups.end(); ++it)
2187     {
2188         int holeNum = QStringView(*it).left((*it).indexOf(QLatin1Char('-'))).toInt();
2189         if (holeNum == curHole)
2190             cfg->deleteGroup(*it);
2191     }
2192     for (QGraphicsItem* qitem : std::as_const(m_topLevelQItems)) {
2193         CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
2194         if (citem)
2195         {
2196             cfgGroup = KConfigGroup(cfg->group(makeGroup(citem->curId(), curHole, citem->name(), (int)qitem->x(), (int)qitem->y())));
2197             citem->save(&cfgGroup);
2198         }
2199     }
2200 
2201     // save where ball starts (whiteBall tells all)
2202     cfgGroup = KConfigGroup(cfg->group(QStringLiteral("%1-ball@%2,%3").arg(curHole).arg((int)whiteBall->x()).arg((int)whiteBall->y())));
2203     cfgGroup.writeEntry("dummykey", true);
2204 
2205     cfgGroup = KConfigGroup(cfg->group(QStringLiteral("0-course@-50,-50")));
2206     cfgGroup.writeEntry("author", holeInfo.author());
2207     cfgGroup.writeEntry("Name", holeInfo.untranslatedName());
2208 
2209     // save hole info
2210     cfgGroup = KConfigGroup(cfg->group(QStringLiteral("%1-hole@-50,-50|0").arg(curHole)));
2211     cfgGroup.writeEntry("par", holeInfo.par());
2212     cfgGroup.writeEntry("maxstrokes", holeInfo.maxStrokes());
2213     cfgGroup.writeEntry("borderWalls", holeInfo.borderWalls());
2214 
2215     cfg->sync();
2216 
2217     setModified(false);
2218 }
2219 
2220 void KolfGame::toggleEditMode()
2221 {
2222     // won't be editing anymore, and user wants to cancel, we return
2223     // this is pretty useless. when the person leaves the hole,
2224     // he gets asked again
2225     /*
2226        if (editing && modified)
2227        {
2228        if (askSave(false))
2229        {
2230        Q_EMIT checkEditing();
2231        return;
2232        }
2233        }
2234        */
2235 
2236     selectedItem = nullptr;
2237 
2238     editing = !editing;
2239 
2240     if (editing)
2241     {
2242         Q_EMIT editingStarted();
2243         setSelectedItem(nullptr);
2244     }
2245     else
2246     {
2247         Q_EMIT editingEnded();
2248         setCursor(Qt::ArrowCursor);
2249     }
2250 
2251     // alert our items
2252     for (QGraphicsItem* qitem : std::as_const(m_topLevelQItems)) {
2253         if (dynamic_cast<Ball*>(qitem)) continue;
2254         CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
2255         if (citem)
2256             citem->editModeChanged(editing);
2257     }
2258 
2259     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
2260     {
2261         // curplayer shouldn't be hidden no matter what
2262         if ((*it).ball()->beginningOfHole() && it != curPlayer)
2263             (*it).ball()->setVisible(false);
2264         else
2265             (*it).ball()->setVisible(!editing);
2266     }
2267 
2268     whiteBall->setVisible(editing);
2269     whiteBall->editModeChanged(editing);
2270 
2271     // shouldn't see putter whilst editing
2272     putter->setVisible(!editing);
2273 
2274     if (editing)
2275         autoSaveTimer->start(autoSaveMsec);
2276     else
2277         autoSaveTimer->stop();
2278 
2279     inPlay = false;
2280 }
2281 
2282 void KolfGame::setSelectedItem(CanvasItem* citem)
2283 {
2284     QGraphicsItem* qitem = dynamic_cast<QGraphicsItem*>(citem);
2285     selectedItem = qitem;
2286     Q_EMIT newSelectedItem(qitem ? citem : &holeInfo);
2287     //deactivate all other overlays
2288     for (QGraphicsItem* otherQitem : std::as_const(m_topLevelQItems)) {
2289         CanvasItem* otherCitem = dynamic_cast<CanvasItem*>(otherQitem);
2290         if (otherCitem && otherCitem != citem)
2291         {
2292             //false = do not create overlay if it does not exist yet
2293             Kolf::Overlay* otherOverlay = otherCitem->overlay(false);
2294             if (otherOverlay)
2295                 otherOverlay->setState(Kolf::Overlay::Passive);
2296         }
2297     }
2298 }
2299 
2300 void HoleInfo::borderWallsChanged(bool yes)
2301 {
2302     m_borderWalls = yes;
2303     game->setBorderWalls(yes);
2304 }
2305 
2306 bool KolfGame::allPlayersDone()
2307 {
2308     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
2309         if ((*it).ball()->curState() != Holed)
2310             return false;
2311 
2312     return true;
2313 }
2314 
2315 void KolfGame::setBorderWalls(bool showing)
2316 {
2317     for (Kolf::Wall* wall : std::as_const(borderWalls))
2318         wall->setVisible(showing);
2319 }
2320 
2321 void KolfGame::setUseAdvancedPutting(bool yes)
2322 {
2323     m_useAdvancedPutting = yes;
2324 
2325     // increase maxStrength in advanced putting mode
2326     if (yes) {
2327         maxStrength = 65;
2328     } else {
2329         maxStrength = 55;
2330         strokeCircle->setVisible(false);
2331     }
2332 }
2333 
2334 void KolfGame::setShowGuideLine(bool yes)
2335 {
2336     putter->setShowGuideLine(yes);
2337 }
2338 
2339 void KolfGame::setSound(bool yes)
2340 {
2341     m_sound = yes;
2342 }
2343 
2344 void KolfGame::courseInfo(CourseInfo &info, const QString& filename)
2345 {
2346     KConfig config(filename);
2347     KConfigGroup configGroup (config.group(QStringLiteral("0-course@-50,-50")));
2348     info.author = configGroup.readEntry("author", info.author);
2349     info.name = configGroup.readEntry("Name", configGroup.readEntry("name", info.name));
2350     info.untranslatedName = configGroup.readEntryUntranslated("Name", configGroup.readEntryUntranslated("name", info.name));
2351 
2352     unsigned int hole = 1;
2353     unsigned int par= 0;
2354     while (1)
2355     {
2356         QString group = QStringLiteral("%1-hole@-50,-50|0").arg(hole);
2357         if (!config.hasGroup(group))
2358         {
2359             hole--;
2360             break;
2361         }
2362 
2363         configGroup = KConfigGroup(config.group(group));
2364         par += configGroup.readEntry("par", 3);
2365 
2366         hole++;
2367     }
2368 
2369     info.par = par;
2370     info.holes = hole;
2371 }
2372 
2373 void KolfGame::scoresFromSaved(KConfig *config, PlayerList &players)
2374 {
2375     KConfigGroup configGroup(config->group(QStringLiteral("0 Saved Game")));
2376     int numPlayers = configGroup.readEntry("Players", 0);
2377     if (numPlayers <= 0)
2378         return;
2379 
2380     for (int i = 1; i <= numPlayers; ++i)
2381     {
2382         // this is same as in kolf.cpp, but we use saved game values
2383         configGroup = KConfigGroup(config->group(QString::number(i)));
2384         players.append(Player());
2385         players.last().ball()->setColor(configGroup.readEntry("Color", "#ffffff"));
2386         players.last().setName(configGroup.readEntry("Name"));
2387         players.last().setId(i);
2388 
2389         const QStringList scores(configGroup.readEntry("Scores",QStringList()));
2390         QList<int> intscores;
2391         for (QStringList::const_iterator it = scores.begin(); it != scores.end(); ++it)
2392             intscores.append((*it).toInt());
2393 
2394         players.last().setScores(intscores);
2395     }
2396 }
2397 
2398 void KolfGame::saveScores(KConfig *config)
2399 {
2400     // wipe out old player info
2401     const QStringList groups = config->groupList();
2402     for (QStringList::const_iterator it = groups.begin(); it != groups.end(); ++it)
2403     {
2404         // this deletes all int groups, ie, the player info groups
2405         bool ok = false;
2406         (*it).toInt(&ok);
2407         if (ok)
2408             config->deleteGroup(*it);
2409     }
2410 
2411     KConfigGroup configGroup(config->group(QStringLiteral("0 Saved Game")));
2412     configGroup.writeEntry("Players", players->count());
2413     configGroup.writeEntry("Course", filename);
2414     configGroup.writeEntry("Current Hole", curHole);
2415 
2416     for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
2417     {
2418         KConfigGroup configGroup(config->group(QString::number((*it).id())));
2419         configGroup.writeEntry("Name", (*it).name());
2420         configGroup.writeEntry("Color", (*it).ball()->color().name());
2421 
2422         QStringList scores;
2423         QList<int> intscores = (*it).scores();
2424         for (QList<int>::Iterator it = intscores.begin(); it != intscores.end(); ++it)
2425             scores.append(QString::number(*it));
2426 
2427         configGroup.writeEntry("Scores", scores);
2428     }
2429 }
2430 
2431 CourseInfo::CourseInfo()
2432     : name(i18n("Course Name")), author(i18n("Course Author")), holes(0), par(0)
2433 {
2434 }
2435 
2436 #include "moc_game.cpp"