1 /*
2     Copyright (C) 2002-2005, Jason Katz-Brown <jasonkb@mit.edu>
3     Copyright 2010 Stefan Majewsky <majewsky@gmx.net>
4 
5     This program is free software; you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation; either version 2 of the License, or
8     (at your option) any later version.
9 
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with this program; if not, write to the Free Software
17     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18 */
19 
20 #include "game.h"
21 #include "itemfactory.h"
22 #include "kcomboboxdialog.h"
23 #include "obstacles.h"
24 #include "shape.h"
25 
26 #include "tagaro/board.h"
27 
28 #include <QHBoxLayout>
29 #include <QApplication>
30 #include <QCheckBox>
31 #include <QFileDialog>
32 #include <QLabel>
33 #include <QMouseEvent>
34 #include <QSpinBox>
35 #include <QStandardPaths>
36 #include <QTimer>
37 #include <QUrl>
38 #include <QRandomGenerator>
39 #include <KGameRenderer>
40 #include <KLineEdit>
41 #include <KLocalizedString>
42 #include <KMessageBox>
43 #include <KgTheme>
44 #include <Box2D/Dynamics/b2Body.h>
45 #include <Box2D/Dynamics/Contacts/b2Contact.h>
46 #include <Box2D/Dynamics/b2Fixture.h>
47 #include <Box2D/Dynamics/b2World.h>
48 #include <Box2D/Dynamics/b2WorldCallbacks.h>
49 
makeGroup(int id,int hole,const QString & name,int x,int y)50 inline QString makeGroup(int id, int hole, const QString &name, int x, int y)
51 {
52 	return QStringLiteral("%1-%2@%3,%4|%5").arg(hole).arg(name).arg(x).arg(y).arg(id);
53 }
54 
makeStateGroup(int id,const QString & name)55 inline QString makeStateGroup(int id, const QString &name)
56 {
57 	return QStringLiteral("%1|%2").arg(name).arg(id);
58 }
59 
60 class KolfContactListener : public b2ContactListener
61 {
62 	public:
PreSolve(b2Contact * contact,const b2Manifold * oldManifold)63 		void PreSolve(b2Contact* contact, const b2Manifold* oldManifold) override
64 		{
65 			Q_UNUSED(oldManifold)
66 			CanvasItem* citemA = static_cast<CanvasItem*>(contact->GetFixtureA()->GetBody()->GetUserData());
67 			CanvasItem* citemB = static_cast<CanvasItem*>(contact->GetFixtureB()->GetBody()->GetUserData());
68 			if (!CanvasItem::mayCollide(citemA, citemB))
69 				contact->SetEnabled(false);
70 		}
71 };
72 
73 class KolfWorld : public b2World
74 {
75 	public:
KolfWorld()76 		KolfWorld()
77 			: b2World(b2Vec2(0, 0), true) //parameters: no gravity, objects are allowed to sleep
78 		{
79 			SetContactListener(&m_listener);
80 		}
81 	private:
82 		KolfContactListener m_listener;
83 };
84 
85 class KolfTheme : public KgTheme
86 {
87 	public:
KolfTheme()88 		KolfTheme() : KgTheme("pics/default_theme.desktop")
89 		{
90 			setGraphicsPath(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("pics/default_theme.svgz")));
91 		}
92 };
93 
94 class KolfRenderer : public KGameRenderer
95 {
96 	public:
KolfRenderer()97 		KolfRenderer() : KGameRenderer(new KolfTheme)
98 		{
99 			setStrategyEnabled(KGameRenderer::UseDiskCache, false);
100 			setStrategyEnabled(KGameRenderer::UseRenderingThreads, false);
101 		}
102 };
103 
Q_GLOBAL_STATIC(KolfRenderer,g_renderer)104 Q_GLOBAL_STATIC(KolfRenderer, g_renderer)
105 Q_GLOBAL_STATIC(KolfWorld, g_world)
106 
107 KGameRenderer* Kolf::renderer()
108 {
109 	return g_renderer;
110 }
111 
findBoard(QGraphicsItem * item_)112 Tagaro::Board* Kolf::findBoard(QGraphicsItem* item_)
113 {
114 	//This returns the toplevel board instance in which the given parent resides.
115 	return item_ ? dynamic_cast<Tagaro::Board*>(item_->topLevelItem()) : nullptr;
116 }
117 
world()118 b2World* Kolf::world()
119 {
120 	return g_world;
121 }
122 
123 /////////////////////////
124 
Putter(QGraphicsItem * parent,b2World * world)125 Putter::Putter(QGraphicsItem* parent, b2World* world)
126 : QGraphicsLineItem(parent)
127 , CanvasItem(world)
128 {
129 	setData(0, Rtti_Putter);
130 	setZBehavior(CanvasItem::FixedZValue, 10001);
131 	m_showGuideLine = true;
132 	oneDegree = M_PI / 180;
133 	guideLineLength = 9;
134 	putterWidth = 11;
135 	angle = 0;
136 
137 	guideLine = new QGraphicsLineItem(this);
138 	guideLine->setPen(QPen(Qt::white));
139 	guideLine->setZValue(998.8);
140 
141 	setPen(QPen(Qt::black, 4));
142 	maxAngle = 2 * M_PI;
143 
144 	hideInfo();
145 
146 	// this also sets Z
147 	resetAngles();
148 }
149 
showInfo()150 void Putter::showInfo()
151 {
152 	guideLine->setVisible(isVisible());
153 }
154 
hideInfo()155 void Putter::hideInfo()
156 {
157 	guideLine->setVisible(m_showGuideLine? isVisible() : false);
158 }
159 
moveBy(double dx,double dy)160 void Putter::moveBy(double dx, double dy)
161 {
162 	QGraphicsLineItem::moveBy(dx, dy);
163 	guideLine->setPos(x(), y());
164 	CanvasItem::moveBy(dx, dy);
165 }
166 
setShowGuideLine(bool yes)167 void Putter::setShowGuideLine(bool yes)
168 {
169 	m_showGuideLine = yes;
170 	setVisible(isVisible());
171 }
172 
setVisible(bool yes)173 void Putter::setVisible(bool yes)
174 {
175 	QGraphicsLineItem::setVisible(yes);
176 	guideLine->setVisible(m_showGuideLine? yes : false);
177 }
178 
setOrigin(double _x,double _y)179 void Putter::setOrigin(double _x, double _y)
180 {
181 	setVisible(true);
182 	setPos(_x, _y);
183 	guideLineLength = 9; //reset to default
184 	finishMe();
185 }
186 
setAngle(Ball * ball)187 void Putter::setAngle(Ball *ball)
188 {
189 	angle = angleMap.contains(ball)? angleMap[ball] : 0;
190 	finishMe();
191 }
192 
go(Direction d,Amount amount)193 void Putter::go(Direction d, Amount amount)
194 {
195 	double addition = (amount == Amount_More? 6 * oneDegree : amount == Amount_Less? .5 * oneDegree : 2 * oneDegree);
196 
197 	switch (d)
198 	{
199 		case Forwards:
200 			guideLineLength -= 1;
201 			guideLine->setVisible(false);
202 			break;
203 		case Backwards:
204 			guideLineLength += 1;
205 			guideLine->setVisible(false);
206 			break;
207 		case D_Left:
208 			angle += addition;
209 			if (angle > maxAngle)
210 				angle -= maxAngle;
211 			break;
212 		case D_Right:
213 			angle -= addition;
214 			if (angle < 0)
215 				angle = maxAngle - fabs(angle);
216 			break;
217 	}
218 
219 	finishMe();
220 }
221 
finishMe()222 void Putter::finishMe()
223 {
224 	midPoint.setX(cos(angle) * guideLineLength);
225 	midPoint.setY(-sin(angle) * guideLineLength);
226 
227 	QPointF start;
228 	QPointF end;
229 
230 	if (midPoint.y() || !midPoint.x())
231 	{
232 		start.setX(midPoint.x() - putterWidth * sin(angle));
233 		start.setY(midPoint.y() - putterWidth * cos(angle));
234 		end.setX(midPoint.x() + putterWidth * sin(angle));
235 		end.setY(midPoint.y() + putterWidth * cos(angle));
236 	}
237 	else
238 	{
239 		start.setX(midPoint.x());
240 		start.setY(midPoint.y() + putterWidth);
241 		end.setY(midPoint.y() - putterWidth);
242 		end.setX(midPoint.x());
243 	}
244 
245 	guideLine->setLine(midPoint.x(), midPoint.y(), -cos(angle) * guideLineLength * 4, sin(angle) * guideLineLength * 4);
246 
247 	setLine(start.x(), start.y(), end.x(), end.y());
248 }
249 
250 /////////////////////////
251 
HoleConfig(HoleInfo * holeInfo,QWidget * parent)252 HoleConfig::HoleConfig(HoleInfo *holeInfo, QWidget *parent)
253 : Config(parent)
254 {
255 	this->holeInfo = holeInfo;
256 
257 	QVBoxLayout *layout = new QVBoxLayout(this);
258 
259 	QHBoxLayout *hlayout = new QHBoxLayout;
260 	layout->addLayout( hlayout );
261 	hlayout->addWidget(new QLabel(i18n("Course name: "), this));
262 	KLineEdit *nameEdit = new KLineEdit(holeInfo->untranslatedName(), this);
263 	hlayout->addWidget(nameEdit);
264 	connect(nameEdit, &KLineEdit::textChanged, this, &HoleConfig::nameChanged);
265 
266 	hlayout = new QHBoxLayout;
267 	layout->addLayout( hlayout );
268 	hlayout->addWidget(new QLabel(i18n("Course author: "), this));
269 	KLineEdit *authorEdit = new KLineEdit(holeInfo->author(), this);
270 	hlayout->addWidget(authorEdit);
271 	connect(authorEdit, &KLineEdit::textChanged, this, &HoleConfig::authorChanged);
272 
273 	layout->addStretch();
274 
275 	hlayout = new QHBoxLayout;
276 	layout->addLayout( hlayout );
277 	hlayout->addWidget(new QLabel(i18n("Par:"), this));
278 	QSpinBox *par = new QSpinBox(this);
279 	par->setRange( 1, 15 );
280 	par->setSingleStep( 1 );
281 	par->setValue(holeInfo->par());
282 	hlayout->addWidget(par);
283         connect(par, qOverload<int>(&QSpinBox::valueChanged), this,
284                 &HoleConfig::parChanged);
285         hlayout->addStretch();
286 
287 	hlayout->addWidget(new QLabel(i18n("Maximum:"), this));
288 	QSpinBox *maxstrokes = new QSpinBox(this);
289 	maxstrokes->setRange( holeInfo->lowestMaxStrokes(), 30 );
290 	maxstrokes->setSingleStep( 1 );
291 	maxstrokes->setWhatsThis( i18n("Maximum number of strokes player can take on this hole."));
292 	maxstrokes->setToolTip( i18n("Maximum number of strokes"));
293 	maxstrokes->setSpecialValueText(i18n("Unlimited"));
294 	maxstrokes->setValue(holeInfo->maxStrokes());
295 	hlayout->addWidget(maxstrokes);
296         connect(maxstrokes, qOverload<int>(&QSpinBox::valueChanged), this,
297                 &HoleConfig::maxStrokesChanged);
298 
299         QCheckBox *check = new QCheckBox(i18n("Show border walls"), this);
300 	check->setChecked(holeInfo->borderWalls());
301 	layout->addWidget(check);
302 	connect(check, &QCheckBox::toggled, this, &HoleConfig::borderWallsChanged);
303 }
304 
authorChanged(const QString & newauthor)305 void HoleConfig::authorChanged(const QString &newauthor)
306 {
307 	holeInfo->setAuthor(newauthor);
308 	changed();
309 }
310 
nameChanged(const QString & newname)311 void HoleConfig::nameChanged(const QString &newname)
312 {
313 	holeInfo->setName(newname);
314 	holeInfo->setUntranslatedName(newname);
315 	changed();
316 }
317 
parChanged(int newpar)318 void HoleConfig::parChanged(int newpar)
319 {
320 	holeInfo->setPar(newpar);
321 	changed();
322 }
323 
maxStrokesChanged(int newms)324 void HoleConfig::maxStrokesChanged(int newms)
325 {
326 	holeInfo->setMaxStrokes(newms);
327 	changed();
328 }
329 
borderWallsChanged(bool yes)330 void HoleConfig::borderWallsChanged(bool yes)
331 {
332 	holeInfo->borderWallsChanged(yes);
333 	changed();
334 }
335 
336 /////////////////////////
337 
StrokeCircle(QGraphicsItem * parent)338 StrokeCircle::StrokeCircle(QGraphicsItem *parent)
339 : QGraphicsItem(parent)
340 {
341 	dvalue = 0;
342 	dmax = 360;
343 	iwidth = 100;
344 	iheight = 100;
345 	ithickness = 8;
346 	setZValue(10000);
347 
348 	setSize(QSizeF(80, 80));
349 	setThickness(8);
350 }
351 
setValue(double v)352 void StrokeCircle::setValue(double v)
353 {
354 	dvalue = v;
355 	if (dvalue > dmax)
356 		dvalue = dmax;
357 
358 	update();
359 }
360 
value()361 double StrokeCircle::value()
362 {
363 	return dvalue;
364 }
365 
collidesWithItem(const QGraphicsItem *,Qt::ItemSelectionMode) const366 bool StrokeCircle::collidesWithItem(const QGraphicsItem*, Qt::ItemSelectionMode) const { return false; }
367 
boundingRect() const368 QRectF StrokeCircle::boundingRect() const { return QRectF(x(), y(), iwidth, iheight); }
369 
setMaxValue(double m)370 void StrokeCircle::setMaxValue(double m)
371 {
372 	dmax = m;
373 	if (dvalue > dmax)
374 		dvalue = dmax;
375 }
setSize(const QSizeF & size)376 void StrokeCircle::setSize(const QSizeF& size)
377 {
378 	if (size.width() > 0)
379 		iwidth = size.width();
380 	if (size.height() > 0)
381 		iheight = size.height();
382 }
setThickness(double t)383 void StrokeCircle::setThickness(double t)
384 {
385 	if (t > 0)
386 		ithickness = t;
387 }
388 
thickness() const389 double StrokeCircle::thickness() const
390 {
391 	return ithickness;
392 }
393 
width() const394 double StrokeCircle::width() const
395 {
396 	return iwidth;
397 }
398 
height() const399 double StrokeCircle::height() const
400 {
401 	return iheight;
402 }
403 
paint(QPainter * p,const QStyleOptionGraphicsItem *,QWidget *)404 void StrokeCircle::paint (QPainter *p, const QStyleOptionGraphicsItem *, QWidget * )
405 {
406 	int al = (int)((dvalue * 360 * 16) / dmax);
407 	int length, deg;
408 	if (al < 0)
409 	{
410 		deg = 270 * 16;
411 		length = -al;
412 	}
413 	else if (al <= (270 * 16))
414 	{
415 		deg = 270 * 16 - al;
416 		length = al;
417 	}
418 	else
419 	{
420 		deg = (360 * 16) - (al - (270 * 16));
421 		length = al;
422 	}
423 
424 	p->setBrush(QBrush(Qt::black, Qt::NoBrush));
425 	p->setPen(QPen(Qt::white, ithickness / 2));
426 	p->drawEllipse(QRectF(x() + ithickness / 2, y() + ithickness / 2, iwidth - ithickness, iheight - ithickness));
427 
428 	if(dvalue>=0)
429 		p->setPen(QPen(QColor((int)((0xff * dvalue) / dmax), 0, (int)(0xff - (0xff * dvalue) / dmax)), ithickness));
430 	else
431 		p->setPen(QPen(QColor("black"), ithickness));
432 
433 	p->drawArc(QRectF(x() + ithickness / 2, y() + ithickness / 2, iwidth - ithickness, iheight - ithickness), deg, length);
434 
435 	p->setPen(QPen(Qt::white, 1));
436 	p->drawEllipse(QRectF(x(), y(), iwidth, iheight));
437 	p->drawEllipse(QRectF(x() + ithickness, y() + ithickness, iwidth - ithickness * 2, iheight - ithickness * 2));
438 	p->setPen(QPen(Qt::white, 3));
439 	p->drawLine(QPointF(x() + iwidth / 2, y() + iheight - ithickness * 1.5), QPointF(x() + iwidth / 2, y() + iheight));
440 	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));
441 	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));
442 }
443 /////////////////////////////////////////
444 
KolfGame(const Kolf::ItemFactory & factory,PlayerList * players,const QString & filename,QWidget * parent)445 KolfGame::KolfGame(const Kolf::ItemFactory& factory, PlayerList *players, const QString &filename, QWidget *parent)
446 : QGraphicsView(parent),
447  m_factory(factory),
448  m_soundBlackHole(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/blackhole.wav"))),
449  m_soundBlackHoleEject(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/blackholeeject.wav"))),
450  m_soundBlackHolePutIn(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/blackholeputin.wav"))),
451  m_soundBumper(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/bumper.wav"))),
452  m_soundHit(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/hit.wav"))),
453  m_soundHoled(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/holed.wav"))),
454  m_soundHoleINone(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/holeinone.wav"))),
455  m_soundPuddle(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/puddle.wav"))),
456  m_soundWall(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/wall.wav"))),
457  m_soundWooHoo(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds/woohoo.wav"))),
458  holeInfo(g_world)
459 {
460 	setRenderHint(QPainter::Antialiasing);
461 	// for mouse control
462 	setMouseTracking(true);
463 	viewport()->setMouseTracking(true);
464 	setFrameShape(NoFrame);
465 
466 	regAdv = false;
467 	curHole = 0; // will get ++'d
468 	cfg = nullptr;
469 	setFilename(filename);
470 	this->players = players;
471 	curPlayer = players->end();
472 	curPlayer--; // will get ++'d to end and sent back
473 	// to beginning
474 	paused = false;
475 	modified = false;
476 	inPlay = false;
477 	putting = false;
478 	stroking = false;
479 	editing = false;
480 	strict = false;
481 	lastDelId = -1;
482 	m_showInfo = false;
483 	ballStateList.canUndo = false;
484 	dontAddStroke = false;
485 	addingNewHole = false;
486 	scoreboardHoles = 0;
487 	infoShown = false;
488 	m_useMouse = true;
489 	m_useAdvancedPutting = true;
490 	m_sound = true;
491 	m_ignoreEvents = false;
492 	highestHole = 0;
493 	recalcHighestHole = false;
494 	banner = nullptr;
495 
496 	holeInfo.setGame(this);
497 	holeInfo.setAuthor(i18n("Course Author"));
498 	holeInfo.setName(i18n("Course Name"));
499 	holeInfo.setUntranslatedName(i18n("Course Name"));
500 	holeInfo.setMaxStrokes(10);
501 	holeInfo.borderWallsChanged(true);
502 
503 	// width and height are the width and height of the scene
504 	// in easy storage
505 	width = 400;
506 	height = 400;
507 
508 	margin = 10;
509 
510 	setFocusPolicy(Qt::StrongFocus);
511 	setMinimumSize(width, height);
512 	QSizePolicy sizePolicy = QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
513 	setSizePolicy(sizePolicy);
514 
515 	setContentsMargins(margin, margin, margin, margin);
516 
517 	course = new Tagaro::Scene(Kolf::renderer(), QStringLiteral("grass"));
518 	course->setMainView(this); //this does this->setScene(course)
519 	courseBoard = new Tagaro::Board;
520 	courseBoard->setLogicalSize(QSizeF(400, 400));
521 	course->addItem(courseBoard);
522 
523 	if( filename.contains( QLatin1String("intro") ) )
524 	{
525 		banner = new Tagaro::SpriteObjectItem(Kolf::renderer(), QStringLiteral("intro_foreground"), courseBoard);
526 		banner->setSize(400, 132);
527 		banner->setPos(0, 32);
528 		banner->setZValue(3); //on the height of a puddle (above slopes and sands, below any objects)
529 	}
530 
531 	adjustSize();
532 
533 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
534 	{
535 		Ball* ball = (*it).ball();
536 		ball->setParentItem(courseBoard);
537 		m_topLevelQItems << ball;
538 		m_moveableQItems << ball;
539 	}
540 
541 	QFont font = QApplication::font();
542 	font.setPixelSize(12);
543 
544 	// create the advanced putting indicator
545 	strokeCircle = new StrokeCircle(courseBoard);
546 	strokeCircle->setPos(width - 90, height - 90);
547 	strokeCircle->setVisible(false);
548 	strokeCircle->setValue(0);
549 	strokeCircle->setMaxValue(360);
550 
551 	// whiteBall marks the spot of the whole whilst editing
552 	whiteBall = new Ball(courseBoard, g_world);
553 	whiteBall->setGame(this);
554 	whiteBall->setColor(Qt::white);
555 	whiteBall->setVisible(false);
556 	whiteBall->setDoDetect(false);
557 	m_topLevelQItems << whiteBall;
558 	m_moveableQItems << whiteBall;
559 
560 	int highestLog = 0;
561 
562 	// if players have scores from loaded game, move to last hole
563 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
564 	{
565 		if ((int)(*it).scores().count() > highestLog)
566 			highestLog = (*it).scores().count();
567 
568 		(*it).ball()->setGame(this);
569 	}
570 
571 	// here only for saved games
572 	if (highestLog)
573 		curHole = highestLog;
574 
575 	putter = new Putter(courseBoard, g_world);
576 
577 	// border walls:
578 
579 	// horiz
580 	addBorderWall(QPoint(margin, margin), QPoint(width - margin, margin));
581 	addBorderWall(QPoint(margin, height - margin - 1), QPoint(width - margin, height - margin - 1));
582 
583 	// vert
584 	addBorderWall(QPoint(margin, margin), QPoint(margin, height - margin));
585 	addBorderWall(QPoint(width - margin - 1, margin), QPoint(width - margin - 1, height - margin));
586 
587 	timer = new QTimer(this);
588 	connect(timer, &QTimer::timeout, this, &KolfGame::timeout);
589 	timerMsec = 300;
590 
591 	fastTimer = new QTimer(this);
592 	connect(fastTimer, &QTimer::timeout, this, &KolfGame::fastTimeout);
593 	fastTimerMsec = 11;
594 
595 	autoSaveTimer = new QTimer(this);
596 	connect(autoSaveTimer, &QTimer::timeout, this, &KolfGame::autoSaveTimeout);
597 	autoSaveMsec = 5 * 1000 * 60; // 5 min autosave
598 
599 	// setUseAdvancedPutting() sets maxStrength!
600 	setUseAdvancedPutting(false);
601 
602 	putting = false;
603 	putterTimer = new QTimer(this);
604 	connect(putterTimer, &QTimer::timeout, this, &KolfGame::putterTimeout);
605 	putterTimerMsec = 20;
606 }
607 
playSound(Sound soundType)608 void KolfGame::playSound(Sound soundType)
609 {
610 	if (m_sound) {
611 		switch (soundType) {
612 			case Sound::BlackHole:
613 				m_soundBlackHole.start();
614 				break;
615 			case Sound::BlackHoleEject:
616 				m_soundBlackHoleEject.start();
617 				break;
618 			case Sound::BlackHolePutIn:
619 				m_soundBlackHolePutIn.start();
620 				break;
621 			case Sound::Bumper:
622 				m_soundBumper.start();
623 				break;
624 			case Sound::Hit:
625 				m_soundHit.start();
626 				break;
627 			case Sound::Holed:
628 				m_soundHoled.start();
629 				break;
630 			case Sound::HoleINone:
631 				m_soundHoleINone.start();
632 				break;
633 			case Sound::Puddle:
634 				m_soundPuddle.start();
635 				break;
636 			case Sound::Wall:
637 				m_soundWall.start();
638 				break;
639 			case Sound::WooHoo:
640 				m_soundWooHoo.start();
641 				break;
642 			default:
643 				qWarning() << "There was a request to play an unknown sound.";
644 				break;
645 		}
646 	}
647 }
648 
startFirstHole(int hole)649 void KolfGame::startFirstHole(int hole)
650 {
651 	if (curHole > 0) // if there was saved game, sync scoreboard
652 		// with number of holes
653 	{
654 		for (; scoreboardHoles < curHole; ++scoreboardHoles)
655 		{
656 			cfgGroup = KConfigGroup(cfg->group(QStringLiteral("%1-hole@-50,-50|0").arg(scoreboardHoles + 1)));
657 			Q_EMIT newHole(cfgGroup.readEntry("par", 3));
658 		}
659 
660 		// lets load all of the scores from saved game if there are any
661 		for (int hole = 1; hole <= curHole; ++hole)
662 			for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
663 				Q_EMIT scoreChanged((*it).id(), hole, (*it).score(hole));
664 	}
665 
666 	curHole = hole - 1;
667 
668 	// this increments curHole, etc
669 	recalcHighestHole = true;
670 	startNextHole();
671 	paused = true;
672 	unPause();
673 }
674 
setFilename(const QString & filename)675 void KolfGame::setFilename(const QString &filename)
676 {
677 	this->filename = filename;
678 	delete cfg;
679 	cfg = new KConfig(filename, KConfig::NoGlobals);
680 }
681 
~KolfGame()682 KolfGame::~KolfGame()
683 {
684 	const QList<QGraphicsItem*> itemsCopy(m_topLevelQItems); //this list will be modified soon, so take a copy
685 	for (QGraphicsItem* item : itemsCopy) {
686 		CanvasItem* citem = dynamic_cast<CanvasItem*>(item);
687 		delete citem;
688 	}
689 
690 	delete cfg;
691 }
692 
setModified(bool mod)693 void KolfGame::setModified(bool mod)
694 {
695 	modified = mod;
696 	Q_EMIT modifiedChanged(mod);
697 }
698 
pause()699 void KolfGame::pause()
700 {
701 	if (paused)
702 	{
703 		// play along with people who call pause() again, instead of unPause()
704 		unPause();
705 		return;
706 	}
707 
708 	paused = true;
709 	timer->stop();
710 	fastTimer->stop();
711 	putterTimer->stop();
712 }
713 
unPause()714 void KolfGame::unPause()
715 {
716 	if (!paused)
717 		return;
718 
719 	paused = false;
720 
721 	timer->start(timerMsec);
722 	fastTimer->start(fastTimerMsec);
723 
724 	if (putting || stroking)
725 		putterTimer->start(putterTimerMsec);
726 }
727 
addBorderWall(const QPoint & start,const QPoint & end)728 void KolfGame::addBorderWall(const QPoint &start, const QPoint &end)
729 {
730 	Kolf::Wall *wall = new Kolf::Wall(courseBoard, g_world);
731 	wall->setLine(QLineF(start, end));
732 	wall->setVisible(true);
733 	wall->setGame(this);
734 	//change Z value to something very high so that border walls
735 	//really keep the balls inside the course
736 	wall->setZBehavior(CanvasItem::FixedZValue, 10000);
737 	borderWalls.append(wall);
738 }
739 
handleMouseDoubleClickEvent(QMouseEvent * e)740 void KolfGame::handleMouseDoubleClickEvent(QMouseEvent *e)
741 {
742 	// allow two fast single clicks
743 	handleMousePressEvent(e);
744 }
745 
handleMousePressEvent(QMouseEvent * e)746 void KolfGame::handleMousePressEvent(QMouseEvent *e)
747 {
748 	if (m_ignoreEvents)
749 		return;
750 
751 	if (editing)
752 	{
753 		//at this point, QGV::mousePressEvent and thus the interaction
754 		//with overlays has already been done; we therefore know that
755 		//the user has clicked into free space
756 		setSelectedItem(nullptr);
757 		return;
758 	}
759 	else
760 	{
761 		if (m_useMouse)
762 		{
763 			if (!inPlay && e->button() == Qt::LeftButton)
764 				puttPress();
765 			else if (e->button() == Qt::RightButton)
766 				toggleShowInfo();
767 		}
768 	}
769 
770 	setFocus();
771 }
772 
viewportToViewport(const QPoint & p)773 QPoint KolfGame::viewportToViewport(const QPoint &p)
774 {
775 	//convert viewport coordinates to board coordinates
776 	return courseBoard->deviceTransform(viewportTransform()).inverted().map(p);
777 }
778 
779 // the following four functions are needed to handle both
780 // border presses and regular in-course presses
781 
mouseReleaseEvent(QMouseEvent * e)782 void KolfGame::mouseReleaseEvent(QMouseEvent * e)
783 {
784 	e->setAccepted(false);
785 	QGraphicsView::mouseReleaseEvent(e);
786 	if (e->isAccepted())
787 		return;
788 
789 	QMouseEvent fixedEvent (QEvent::MouseButtonRelease, viewportToViewport(e->pos()), e->button(), e->buttons(), e->modifiers());
790 	handleMouseReleaseEvent(&fixedEvent);
791 	e->accept();
792 }
793 
mousePressEvent(QMouseEvent * e)794 void KolfGame::mousePressEvent(QMouseEvent * e)
795 {
796 	e->setAccepted(false);
797 	QGraphicsView::mousePressEvent(e);
798 	if (e->isAccepted())
799 		return;
800 
801 	QMouseEvent fixedEvent (QEvent::MouseButtonPress, viewportToViewport(e->pos()), e->button(), e->buttons(), e->modifiers());
802 	handleMousePressEvent(&fixedEvent);
803 	e->accept();
804 }
805 
mouseDoubleClickEvent(QMouseEvent * e)806 void KolfGame::mouseDoubleClickEvent(QMouseEvent * e)
807 {
808 	e->setAccepted(false);
809 	QGraphicsView::mouseDoubleClickEvent(e);
810 	if (e->isAccepted())
811 		return;
812 
813 	QMouseEvent fixedEvent (QEvent::MouseButtonDblClick, viewportToViewport(e->pos()), e->button(), e->buttons(), e->modifiers());
814 	handleMouseDoubleClickEvent(&fixedEvent);
815 	e->accept();
816 }
817 
mouseMoveEvent(QMouseEvent * e)818 void KolfGame::mouseMoveEvent(QMouseEvent * e)
819 {
820 	e->setAccepted(false);
821 	QGraphicsView::mouseMoveEvent(e);
822 	if (e->isAccepted())
823 		return;
824 
825 	QMouseEvent fixedEvent (QEvent::MouseMove, viewportToViewport(e->pos()), e->button(), e->buttons(), e->modifiers());
826 	handleMouseMoveEvent(&fixedEvent);
827 	e->accept();
828 }
829 
handleMouseMoveEvent(QMouseEvent * e)830 void KolfGame::handleMouseMoveEvent(QMouseEvent *e)
831 {
832 	if (!editing && !inPlay && putter && !m_ignoreEvents)
833 	{
834 		// mouse moving of putter
835 		updateMouse();
836 		e->accept();
837 	}
838 }
839 
updateMouse()840 void KolfGame::updateMouse()
841 {
842 	// don't move putter if in advanced putting sequence
843 	if (!m_useMouse || ((stroking || putting) && m_useAdvancedPutting))
844 		return;
845 
846 	const QPointF cursor = viewportToViewport(mapFromGlobal(QCursor::pos()));
847 	const QPointF ball((*curPlayer).ball()->x(), (*curPlayer).ball()->y());
848 	putter->setAngle(-Vector(cursor - ball).direction());
849 }
850 
handleMouseReleaseEvent(QMouseEvent * e)851 void KolfGame::handleMouseReleaseEvent(QMouseEvent *e)
852 {
853 	setCursor(Qt::ArrowCursor);
854 
855 	if (editing)
856 	{
857 		Q_EMIT newStatusText(QString());
858 	}
859 
860 	if (m_ignoreEvents)
861 		return;
862 
863 	if (!editing && m_useMouse)
864 	{
865 		if (!inPlay && e->button() == Qt::LeftButton)
866 			puttRelease();
867 		else if (e->button() == Qt::RightButton)
868 			toggleShowInfo();
869 	}
870 
871 	setFocus();
872 }
873 
keyPressEvent(QKeyEvent * e)874 void KolfGame::keyPressEvent(QKeyEvent *e)
875 {
876 	if (inPlay || editing || m_ignoreEvents)
877 		return;
878 
879 	switch (e->key())
880 	{
881 		case Qt::Key_Up:
882 			if (!e->isAutoRepeat())
883 				toggleShowInfo();
884 			break;
885 
886 		case Qt::Key_Escape:
887 			putting = false;
888 			stroking = false;
889 			finishStroking = false;
890 			strokeCircle->setVisible(false);
891 			putterTimer->stop();
892 			putter->setOrigin((*curPlayer).ball()->x(), (*curPlayer).ball()->y());
893 			break;
894 
895 		case Qt::Key_Left:
896 		case Qt::Key_Right:
897 			// don't move putter if in advanced putting sequence
898 			if ((!stroking && !putting) || !m_useAdvancedPutting)
899 				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);
900 			break;
901 
902 		case Qt::Key_Space: case Qt::Key_Down:
903 			puttPress();
904 			break;
905 
906 		default:
907 			break;
908 	}
909 }
910 
toggleShowInfo()911 void KolfGame::toggleShowInfo()
912 {
913 	setShowInfo(!m_showInfo);
914 }
915 
updateShowInfo()916 void KolfGame::updateShowInfo()
917 {
918 	setShowInfo(m_showInfo);
919 }
920 
setShowInfo(bool yes)921 void KolfGame::setShowInfo(bool yes)
922 {
923 	m_showInfo = yes;
924 	QList<QGraphicsItem*> infoItems;
925 	for (QGraphicsItem* qitem : std::as_const(m_topLevelQItems)) {
926 		CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
927 		if (citem)
928 			infoItems << citem->infoItems();
929 	}
930 	for (QGraphicsItem* qitem : std::as_const(infoItems))
931 		qitem->setVisible(m_showInfo);
932 }
933 
puttPress()934 void KolfGame::puttPress()
935 {
936 	// Advanced putting: 1st click start putting sequence, 2nd determine strength, 3rd determine precision
937 
938 	if (!putting && !stroking && !inPlay)
939 	{
940 		puttCount = 0;
941 		puttReverse = false;
942 		putting = true;
943 		stroking = false;
944 		strength = 0;
945 		if (m_useAdvancedPutting)
946 		{
947 			strokeCircle->setValue(0);
948 			int pw = (int)(putter->line().x2() - putter->line().x1());
949 			if (pw < 0) pw = -pw;
950 			int px = (int)putter->x() + pw / 2;
951 			int py = (int)putter->y();
952 			if (px > width / 2 && py < height / 2)
953 				strokeCircle->setPos(px/2 - pw / 2 - 5 - strokeCircle->width()/2, py/2 + 5);
954 			else if (px > width / 2)
955 				strokeCircle->setPos(px/2 - pw / 2 - 5 - strokeCircle->width()/2, py/2 - 5 - strokeCircle->height()/2);
956 			else if (py < height / 2)
957 				strokeCircle->setPos(px/2 + pw / 2 + 5, py/2 + 5);
958 			else
959 				strokeCircle->setPos(px/2 + pw / 2 + 5, py/2 - 5 - strokeCircle->height()/2);
960 			strokeCircle->setVisible(true);
961 		}
962 		putterTimer->start(putterTimerMsec);
963 	}
964 	else if (m_useAdvancedPutting && putting && !editing)
965 	{
966 		putting = false;
967 		stroking = true;
968 		puttReverse = false;
969 		finishStroking = false;
970 	}
971 	else if (m_useAdvancedPutting && stroking)
972 	{
973 		finishStroking = true;
974 		putterTimeout();
975 	}
976 }
977 
keyReleaseEvent(QKeyEvent * e)978 void KolfGame::keyReleaseEvent(QKeyEvent *e)
979 {
980 	if (e->isAutoRepeat() || m_ignoreEvents)
981 		return;
982 
983 	if (e->key() == Qt::Key_Space || e->key() == Qt::Key_Down)
984 		puttRelease();
985 	else if ((e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete) && !(e->modifiers() & Qt::ControlModifier))
986 	{
987 		if (editing && selectedItem)
988 		{
989 			CanvasItem *citem = dynamic_cast<CanvasItem *>(selectedItem);
990 			if (!citem)
991 				return;
992 			QGraphicsItem *item = dynamic_cast<QGraphicsItem *>(citem);
993 			if (citem && !dynamic_cast<Ball*>(item))
994 			{
995 				lastDelId = citem->curId();
996 
997 				m_topLevelQItems.removeAll(item);
998 				m_moveableQItems.removeAll(item);
999 				delete citem;
1000 				setSelectedItem(nullptr);
1001 
1002 				setModified(true);
1003 			}
1004 		}
1005 	}
1006 	else if (e->key() == Qt::Key_I || e->key() == Qt::Key_Up)
1007 		toggleShowInfo();
1008 }
1009 
resizeEvent(QResizeEvent * ev)1010 void KolfGame::resizeEvent( QResizeEvent* ev )
1011 {
1012 	int newW = ev->size().width();
1013 	int newH = ev->size().height();
1014 	int oldW = ev->oldSize().width();
1015 	int oldH = ev->oldSize().height();
1016 
1017 	if(oldW<=0 || oldH<=0) //this is the first draw so no point wasting resources resizing yet
1018 		return;
1019 	else if( (oldW==newW) && (oldH==newH) )
1020 		return;
1021 
1022 	int setSize = qMin(newW, newH);
1023 	QGraphicsView::resize(setSize, setSize); //make sure new size is square
1024 }
1025 
puttRelease()1026 void KolfGame::puttRelease()
1027 {
1028 	if (!m_useAdvancedPutting && putting && !editing)
1029 	{
1030 		putting = false;
1031 		stroking = true;
1032 	}
1033 }
1034 
stoppedBall()1035 void KolfGame::stoppedBall()
1036 {
1037 	if (!inPlay)
1038 	{
1039 		inPlay = true;
1040 		dontAddStroke = true;
1041 	}
1042 }
1043 
timeout()1044 void KolfGame::timeout()
1045 {
1046 	Ball *curBall = (*curPlayer).ball();
1047 
1048 	// test if the ball is gone
1049 	// in this case we want to stop the ball and
1050 	// later undo the shot
1051 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1052 	{
1053                 //QGV handles management of dirtied rects for us
1054 		//course->update();
1055 
1056 		if (!QRectF(QPointF(), courseBoard->logicalSize()).contains((*it).ball()->pos()))
1057 		{
1058 			(*it).ball()->setState(Stopped);
1059 
1060 			// don't do it if he's past maxStrokes
1061 			if ((*it).score(curHole) < holeInfo.maxStrokes() - 1 || !holeInfo.hasMaxStrokes())
1062 			{
1063 				loadStateList();
1064 			}
1065 			shotDone();
1066 
1067 			return;
1068 		}
1069 	}
1070 
1071 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1072 		if ((*it).ball()->forceStillGoing() || ((*it).ball()->curState() == Rolling && Vector((*it).ball()->velocity()).magnitude() > 0 && (*it).ball()->isVisible()))
1073 			return;
1074 
1075 	int curState = curBall->curState();
1076 	if (curState == Stopped && inPlay)
1077 	{
1078 		inPlay = false;
1079 		QTimer::singleShot(0, this, &KolfGame::shotDone);
1080 	}
1081 
1082 	if (curState == Holed && inPlay)
1083 	{
1084 		Q_EMIT inPlayEnd();
1085 
1086 		int curScore = (*curPlayer).score(curHole);
1087 		if (!dontAddStroke)
1088 			curScore++;
1089 
1090 		if (curScore == 1)
1091 		{
1092 			playSound(Sound::HoleINone);
1093 		}
1094 		else if (curScore <= holeInfo.par())
1095 		{
1096 			playSound(Sound::WooHoo);
1097 		}
1098 
1099 		(*curPlayer).ball()->setZValue((*curPlayer).ball()->zValue() + .1 - (.1)/(curScore));
1100 
1101 		if (allPlayersDone())
1102 		{
1103 			inPlay = false;
1104 
1105 			if (curHole > 0 && !dontAddStroke)
1106 			{
1107 				(*curPlayer).addStrokeToHole(curHole);
1108 				Q_EMIT scoreChanged((*curPlayer).id(), curHole, (*curPlayer).score(curHole));
1109 			}
1110 			QTimer::singleShot(600, this, &KolfGame::holeDone);
1111 		}
1112 		else
1113 		{
1114 			inPlay = false;
1115 			QTimer::singleShot(0, this, &KolfGame::shotDone);
1116 		}
1117 	}
1118 }
1119 
fastTimeout()1120 void KolfGame::fastTimeout()
1121 {
1122 	// do regular advance every other time
1123 	if (regAdv)
1124 		course->advance();
1125 	regAdv = !regAdv;
1126 
1127 	if (editing)
1128 		return;
1129 
1130 	// do Box2D advance
1131 	//Because there are so much CanvasItems out there, there is currently no
1132 	//easy and/or systematic approach to iterate over all of them, except for
1133 	//using the b2Bodies available on the world.
1134 
1135 	//prepare simulation
1136 	for (b2Body* body = g_world->GetBodyList(); body; body = body->GetNext())
1137 	{
1138 		CanvasItem* citem = static_cast<CanvasItem*>(body->GetUserData());
1139 		if (citem)
1140 		{
1141 			citem->startSimulation();
1142 			//HACK: the following should not be necessary at this point
1143 			QGraphicsItem* qitem = dynamic_cast<QGraphicsItem*>(citem);
1144 			if (qitem)
1145 				citem->updateZ(qitem);
1146 		}
1147 	}
1148 	//step world
1149 	//NOTE: I previously set timeStep to 1.0 so that CItem's velocity()
1150 	//corresponds to the position change per step. In this case, the
1151 	//velocity would be scaled by Kolf::Box2DScaleFactor, which would result in
1152 	//very small velocities (below Box2D's internal cutoff thresholds!) for
1153 	//usual movements. Therefore, we apply the scaling to the timestep instead.
1154 	const double timeStep = 1.0 * Kolf::Box2DScaleFactor;
1155 	g_world->Step(timeStep, 10, 10); //parameters 2/3 = iteration counts (TODO: optimize)
1156 	//conclude simulation
1157 	for (b2Body* body = g_world->GetBodyList(); body; body = body->GetNext())
1158 	{
1159 		CanvasItem* citem = static_cast<CanvasItem*>(body->GetUserData());
1160 		if (citem)
1161 		{
1162 			citem->endSimulation();
1163 		}
1164 	}
1165 }
1166 
ballMoved()1167 void KolfGame::ballMoved()
1168 {
1169 	if (putter->isVisible())
1170 	{
1171 		putter->setPos((*curPlayer).ball()->x(), (*curPlayer).ball()->y());
1172 		updateMouse();
1173 	}
1174 }
1175 
putterTimeout()1176 void KolfGame::putterTimeout()
1177 {
1178 	if (inPlay || editing)
1179 		return;
1180 
1181 	if (m_useAdvancedPutting)
1182 	{
1183 		if (putting)
1184 		{
1185 			const qreal base = 2.0;
1186 
1187 			if (puttReverse && strength <= 0)
1188 			{
1189 				// aborted
1190 				putting = false;
1191 				strokeCircle->setVisible(false);
1192 			}
1193 			else if (strength > maxStrength || puttReverse)
1194 			{
1195 				// decreasing strength as we've reached the top
1196 				puttReverse = true;
1197 				strength -= pow(base, qreal(strength / maxStrength)) - 1.8;
1198 				if ((int)strength < puttCount * 2)
1199 				{
1200 					puttCount--;
1201 					if (puttCount >= 0)
1202 						putter->go(Forwards);
1203 				}
1204 			}
1205 			else
1206 			{
1207 				// make the increase at high strength faster
1208 				strength += pow(base, strength / maxStrength) - .3;
1209 				if ((int)strength > puttCount * 2)
1210 				{
1211 					putter->go(Backwards);
1212 					puttCount++;
1213 				}
1214 			}
1215 			// make the visible steps at high strength smaller
1216 			strokeCircle->setValue(pow(strength / maxStrength, 0.8) * 360);
1217 		}
1218 		else if (stroking)
1219 		{
1220 			double al = strokeCircle->value();
1221 			if (al >= 45)
1222 				al -= 0.2 + strength / 50 + al / 100;
1223 			else
1224 				al -= 0.2 + strength / 50;
1225 
1226 			if (puttReverse)
1227 			{
1228 				// show the stroke
1229 				puttCount--;
1230 				if (puttCount >= 0)
1231 					putter->go(Forwards);
1232 				else
1233 				{
1234 					strokeCircle->setVisible(false);
1235 					finishStroking = false;
1236 					putterTimer->stop();
1237 					putting = false;
1238 					stroking = false;
1239 					shotStart();
1240 				}
1241 			}
1242 			else if (al < -45 || finishStroking)
1243 			{
1244 				strokeCircle->setValue(al);
1245 				int deg;
1246 		                auto *generator = QRandomGenerator::global();
1247 				// if > 45 or < -45 then bad stroke
1248 				if (al > 45)
1249 				{
1250 					deg = putter->curDeg() - 45 + rand() % 90;
1251                 			strength -= generator->bounded((int)strength);
1252 				}
1253 				else if (!finishStroking)
1254 				{
1255 					deg = putter->curDeg() - 45 + rand() % 90;
1256 			                strength -= generator->bounded((int)strength);
1257 				}
1258 				else
1259 					deg = putter->curDeg() + (int)(strokeCircle->value() / 3);
1260 
1261 				if (deg < 0)
1262 					deg += 360;
1263 				else if (deg > 360)
1264 					deg -= 360;
1265 
1266 				putter->setDeg(deg);
1267 				puttReverse = true;
1268 			}
1269 			else
1270 			{
1271 				strokeCircle->setValue(al);
1272 				putterTimer->start(putterTimerMsec/10);
1273 			}
1274 		}
1275 	}
1276 	else
1277 	{
1278 		if (putting)
1279 		{
1280 			putter->go(Backwards);
1281 			puttCount++;
1282 			strength += 1.5;
1283 			if (strength > maxStrength)
1284 			{
1285 				putting = false;
1286 				stroking = true;
1287 			}
1288 		}
1289 		else if (stroking)
1290 		{
1291 			if (putter->curLen() < (*curPlayer).ball()->height() + 2)
1292 			{
1293 				stroking = false;
1294 				putterTimer->stop();
1295 				putting = false;
1296 				stroking = false;
1297 				shotStart();
1298 			}
1299 
1300 			putter->go(Forwards);
1301 			putterTimer->start(putterTimerMsec/10);
1302 		}
1303 	}
1304 }
1305 
autoSaveTimeout()1306 void KolfGame::autoSaveTimeout()
1307 {
1308 	// this should be a config option
1309 	// until it is i'll disable it
1310 	if (editing)
1311 	{
1312 		//save();
1313 	}
1314 }
1315 
recreateStateList()1316 void KolfGame::recreateStateList()
1317 {
1318 	savedState.clear();
1319 	for (QGraphicsItem* item : std::as_const(m_topLevelQItems)) {
1320 		if (dynamic_cast<Ball*>(item)) continue; //see below
1321 		CanvasItem* citem = dynamic_cast<CanvasItem*>(item);
1322 		if (citem)
1323 		{
1324 			const QString key = makeStateGroup(citem->curId(), citem->name());
1325 			savedState.insert(key, item->pos());
1326 		}
1327 	}
1328 
1329 	ballStateList.clear();
1330 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1331 		ballStateList.append((*it).stateInfo(curHole));
1332 
1333 	ballStateList.canUndo = true;
1334 }
1335 
undoShot()1336 void KolfGame::undoShot()
1337 {
1338 	if (ballStateList.canUndo)
1339 		loadStateList();
1340 }
1341 
loadStateList()1342 void KolfGame::loadStateList()
1343 {
1344 	for (QGraphicsItem* item : std::as_const(m_topLevelQItems)) {
1345 		if (dynamic_cast<Ball*>(item)) continue; //see below
1346 		CanvasItem* citem = dynamic_cast<CanvasItem*>(item);
1347 		if (citem)
1348 		{
1349 			const QString key = makeStateGroup(citem->curId(), citem->name());
1350 			const QPointF currentPos = item->pos();
1351 			const QPointF posDiff = savedState.value(key, currentPos) - currentPos;
1352 			citem->moveBy(posDiff.x(), posDiff.y());
1353 		}
1354 	}
1355 
1356 	for (BallStateList::Iterator it = ballStateList.begin(); it != ballStateList.end(); ++it)
1357 	{
1358 		BallStateInfo info = (*it);
1359 		Player &player = (*(players->begin() + (info.id - 1) ));
1360 		player.ball()->setPos(info.spot.x(), info.spot.y());
1361 		player.ball()->setBeginningOfHole(info.beginningOfHole);
1362 		if ((*curPlayer).id() == info.id)
1363 			ballMoved();
1364 		else
1365 			player.ball()->setVisible(!info.beginningOfHole);
1366 		player.setScoreForHole(info.score, curHole);
1367 		player.ball()->setState(info.state);
1368 		Q_EMIT scoreChanged(info.id, curHole, info.score);
1369 	}
1370 }
1371 
shotDone()1372 void KolfGame::shotDone()
1373 {
1374 	inPlay = false;
1375 	Q_EMIT inPlayEnd();
1376 	setFocus();
1377 
1378 	Ball *ball = (*curPlayer).ball();
1379 
1380 	if(ball->curState() == Rolling) {
1381 		// 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
1382 		// 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
1383 		// before adding a stroke to the player
1384 		inPlay = true;
1385 		return;
1386 	}
1387 
1388 	if (!dontAddStroke && (*curPlayer).numHoles())
1389 		(*curPlayer).addStrokeToHole(curHole);
1390 
1391 	dontAddStroke = false;
1392 
1393 	// do hack stuff, shouldn't be done here
1394 
1395 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1396 	{
1397 		if ((*it).ball()->addStroke())
1398 		{
1399 			for (int i = 1; i <= (*it).ball()->addStroke(); ++i)
1400 				(*it).addStrokeToHole(curHole);
1401 
1402 			// emit that we have a new stroke count
1403 			Q_EMIT scoreChanged((*it).id(), curHole, (*it).score(curHole));
1404 		}
1405 		(*it).ball()->setAddStroke(0);
1406 	}
1407 
1408 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1409 	{
1410 		Ball *ball = (*it).ball();
1411 
1412 		if (ball->curState() == Holed)
1413 			continue;
1414 
1415 		Vector oldVelocity;
1416 		if (ball->placeOnGround(oldVelocity))
1417 		{
1418 			ball->setPlaceOnGround(false);
1419 
1420 			QStringList options;
1421 			const QString placeOutside = i18n("Drop Outside of Hazard");
1422 			const QString rehit = i18n("Rehit From Last Location");
1423 			options << placeOutside << rehit;
1424 			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"));
1425 
1426 			if (choice == placeOutside)
1427 			{
1428 				(*it).ball()->setDoDetect(false);
1429 
1430 				QPointF pos = ball->pos();
1431 				//normalize old velocity
1432 				const QPointF v = oldVelocity / oldVelocity.magnitude();
1433 
1434 				while (1)
1435 				{
1436 					QList<QGraphicsItem *> list = ball->collidingItems();
1437 					bool keepMoving = false;
1438 					while (!list.isEmpty())
1439 					{
1440 						QGraphicsItem *item = list.takeFirst();
1441 						if (item->data(0) == Rtti_DontPlaceOn)
1442 							keepMoving = true;
1443 					}
1444 					if (!keepMoving)
1445 						break;
1446 
1447 					const qreal movePixel = 3.0;
1448 					pos -= v * movePixel;
1449 					ball->setPos(pos);
1450 				}
1451 			}
1452 			else if (choice == rehit)
1453 			{
1454 				for (BallStateList::Iterator it = ballStateList.begin(); it != ballStateList.end(); ++it)
1455 				{
1456 					if ((*it).id == (*curPlayer).id())
1457 					{
1458 						if ((*it).beginningOfHole)
1459 							ball->setPos(whiteBall->x(), whiteBall->y());
1460 						else
1461 							ball->setPos((*it).spot.x(), (*it).spot.y());
1462 
1463 						break;
1464 					}
1465 				}
1466 			}
1467 
1468 			ball->setVisible(true);
1469 			ball->setState(Stopped);
1470 
1471 			(*it).ball()->setDoDetect(true);
1472 			ball->collisionDetect();
1473 		}
1474 	}
1475 
1476 	// emit again
1477 	Q_EMIT scoreChanged((*curPlayer).id(), curHole, (*curPlayer).score(curHole));
1478 
1479 	if(ball->curState() == Rolling) {
1480 		inPlay = true;
1481 		return;
1482 	}
1483 
1484 	ball->setVelocity(Vector());
1485 
1486 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1487 	{
1488 		Ball *ball = (*it).ball();
1489 
1490 		int curStrokes = (*it).score(curHole);
1491 		if (curStrokes >= holeInfo.maxStrokes() && holeInfo.hasMaxStrokes())
1492 		{
1493 			ball->setState(Holed);
1494 			ball->setVisible(false);
1495 
1496 			// move to center in case he/she hit out
1497 			ball->setPos(width / 2, height / 2);
1498 			playerWhoMaxed = (*it).name();
1499 
1500 			if (allPlayersDone())
1501 			{
1502 				startNextHole();
1503 				QTimer::singleShot(100, this, &KolfGame::emitMax);
1504 				return;
1505 			}
1506 
1507 			QTimer::singleShot(100, this, &KolfGame::emitMax);
1508 		}
1509 	}
1510 
1511 	// change player to next player
1512 	// skip player if he's Holed
1513 	do
1514 	{
1515 		curPlayer++;
1516 		if (curPlayer == players->end())
1517 			curPlayer = players->begin();
1518 	}
1519 	while ((*curPlayer).ball()->curState() == Holed);
1520 
1521 	Q_EMIT newPlayersTurn(&(*curPlayer));
1522 
1523 	(*curPlayer).ball()->setVisible(true);
1524 
1525 	inPlay = false;
1526 	(*curPlayer).ball()->collisionDetect();
1527 
1528 	putter->setAngle((*curPlayer).ball());
1529 	putter->setOrigin((*curPlayer).ball()->x(), (*curPlayer).ball()->y());
1530 	updateMouse();
1531 }
1532 
emitMax()1533 void KolfGame::emitMax()
1534 {
1535 	Q_EMIT maxStrokesReached(playerWhoMaxed);
1536 }
1537 
startBall(const Vector & velocity)1538 void KolfGame::startBall(const Vector &velocity)
1539 {
1540 	playSound(Sound::Hit);
1541 	Q_EMIT inPlayStart();
1542 	putter->setVisible(false);
1543 
1544 	(*curPlayer).ball()->setState(Rolling);
1545 	(*curPlayer).ball()->setVelocity(velocity);
1546 	(*curPlayer).ball()->shotStarted();
1547 
1548 	for (QGraphicsItem* qitem : std::as_const(m_topLevelQItems)) {
1549 		CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
1550 		if (citem)
1551 			citem->shotStarted();
1552 	}
1553 
1554 	inPlay = true;
1555 }
1556 
shotStart()1557 void KolfGame::shotStart()
1558 {
1559 	// ensure we never hit the ball back into the hole which
1560 	// can cause hole skippage
1561 	if ((*curPlayer).ball()->curState() == Holed)
1562 		return;
1563 
1564 	// save state
1565 	recreateStateList();
1566 
1567 	putter->saveAngle((*curPlayer).ball());
1568 	strength /= 8;
1569 	if (!strength)
1570 		strength = 1;
1571 
1572 	//kDebug(12007) << "Start started. BallX:" << (*curPlayer).ball()->x() << ", BallY:" << (*curPlayer).ball()->y() << ", Putter Angle:" << putter->curAngle() << ", Vector Strength: " << strength;
1573 
1574 	(*curPlayer).ball()->collisionDetect();
1575 
1576 	startBall(Vector::fromMagnitudeDirection(strength, -(putter->curAngle() + M_PI)));
1577 
1578 	addHoleInfo(ballStateList);
1579 }
1580 
addHoleInfo(BallStateList & list)1581 void KolfGame::addHoleInfo(BallStateList &list)
1582 {
1583 	list.player = (*curPlayer).id();
1584 	list.vector = (*curPlayer).ball()->velocity();
1585 	list.hole = curHole;
1586 }
1587 
sayWhosGoing()1588 void KolfGame::sayWhosGoing()
1589 {
1590 	if (players->count() >= 2)
1591 	{
1592 		KMessageBox::information(this, i18n("%1 will start off.", (*curPlayer).name()), i18n("New Hole"), QStringLiteral("newHole"));
1593 	}
1594 }
1595 
holeDone()1596 void KolfGame::holeDone()
1597 {
1598 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1599 		(*it).ball()->setVisible(false);
1600 	startNextHole();
1601 	sayWhosGoing();
1602 }
1603 
1604 // this function is WAY too smart for it's own good
1605 // ie, bad design :-(
startNextHole()1606 void KolfGame::startNextHole()
1607 {
1608 	setFocus();
1609 
1610 	bool reset = true;
1611 	if (askSave(true))
1612 	{
1613 		if (allPlayersDone())
1614 		{
1615 			// we'll reload this hole, but not reset
1616 			curHole--;
1617 			reset = false;
1618 		}
1619 		else
1620 			return;
1621 	}
1622 	else
1623 		setModified(false);
1624 
1625 	pause();
1626 
1627 	dontAddStroke = false;
1628 
1629 	inPlay = false;
1630 	timer->stop();
1631 	putter->resetAngles();
1632 
1633 	int oldCurHole = curHole;
1634 	curHole++;
1635 	Q_EMIT currentHole(curHole);
1636 
1637 	if (reset)
1638 	{
1639 		whiteBall->setPos(width/2, height/2);
1640 		holeInfo.borderWallsChanged(true);
1641 	}
1642 
1643 	int leastScore = INT_MAX;
1644 
1645 	// to get the first player to go first on every hole,
1646 	// don't do the score stuff below
1647 	curPlayer = players->begin();
1648 
1649 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1650 	{
1651 		if (curHole > 1)
1652 		{
1653 			bool ahead = false;
1654 			if ((*it).lastScore() != 0)
1655 			{
1656 				if ((*it).lastScore() < leastScore)
1657 					ahead = true;
1658 				else if ((*it).lastScore() == leastScore)
1659 				{
1660 					for (int i = curHole - 1; i > 0; --i)
1661 					{
1662 						while(i > (*it).scores().size())
1663 							i--;
1664 
1665 						const int thisScore = (*it).score(i);
1666 						const int thatScore = (*curPlayer).score(i);
1667 						if (thisScore < thatScore)
1668 						{
1669 							ahead = true;
1670 							break;
1671 						}
1672 						else if (thisScore > thatScore)
1673 							break;
1674 					}
1675 				}
1676 			}
1677 
1678 			if (ahead)
1679 			{
1680 				curPlayer = it;
1681 				leastScore = (*it).lastScore();
1682 			}
1683 		}
1684 
1685 		if (reset)
1686 			(*it).ball()->setPos(width / 2, height / 2);
1687 		else
1688 			(*it).ball()->setPos(whiteBall->x(), whiteBall->y());
1689 
1690 		(*it).ball()->setState(Stopped);
1691 
1692 		// this gets set to false when the ball starts
1693 		// to move by the Mr. Ball himself.
1694 		(*it).ball()->setBeginningOfHole(true);
1695 		if ((int)(*it).scores().count() < curHole)
1696 			(*it).addHole();
1697 		(*it).ball()->setVelocity(Vector());
1698 		(*it).ball()->setVisible(false);
1699 	}
1700 
1701 	Q_EMIT newPlayersTurn(&(*curPlayer));
1702 
1703 	if (reset)
1704 		openFile();
1705 
1706 	inPlay = false;
1707 	timer->start(timerMsec);
1708 
1709 	if(size().width()!=400 || size().height()!=400) { //not default size, so resizing needed
1710 		int setSize = qMin(size().width(), size().height());
1711 		//resize needs to be called for setSize+1 first because otherwise it doesn't seem to get called (not sure why)
1712 		QGraphicsView::resize(setSize+1, setSize+1);
1713 		QGraphicsView::resize(setSize, setSize);
1714 	}
1715 
1716 	// if (false) { we're done with the round! }
1717 	if (oldCurHole != curHole)
1718 	{
1719 		for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it) {
1720 			(*it).ball()->setPlaceOnGround(false);
1721 			while( (*it).numHoles() < (unsigned)curHole)
1722 				(*it).addHole();
1723 		}
1724 
1725 		// here we have to make sure the scoreboard shows
1726 		// all of the holes up until now;
1727 
1728 		for (; scoreboardHoles < curHole; ++scoreboardHoles)
1729 		{
1730 			cfgGroup = KConfigGroup(cfg->group(QStringLiteral("%1-hole@-50,-50|0").arg(scoreboardHoles + 1)));
1731 			Q_EMIT newHole(cfgGroup.readEntry("par", 3));
1732 		}
1733 
1734 		resetHoleScores();
1735 		updateShowInfo();
1736 
1737 		// this is from shotDone()
1738 		(*curPlayer).ball()->setVisible(true);
1739 		putter->setOrigin((*curPlayer).ball()->x(), (*curPlayer).ball()->y());
1740 		updateMouse();
1741 
1742 		ballStateList.canUndo = false;
1743 
1744 		(*curPlayer).ball()->collisionDetect();
1745 	}
1746 
1747 	unPause();
1748 }
1749 
showInfoDlg(bool addDontShowAgain)1750 void KolfGame::showInfoDlg(bool addDontShowAgain)
1751 {
1752 	KMessageBox::information(parentWidget(),
1753 			i18n("Course name: %1", holeInfo.name()) + QStringLiteral("\n")
1754 			+ i18n("Created by %1", holeInfo.author()) + QStringLiteral("\n")
1755 			+ i18np("%1 hole", "%1 holes", highestHole),
1756 			i18nc("@title:window", "Course Information"),
1757 			addDontShowAgain? holeInfo.name() + QStringLiteral(" ") + holeInfo.author() : QString());
1758 }
1759 
openFile()1760 void KolfGame::openFile()
1761 {
1762 	QList<QGraphicsItem*> newTopLevelQItems;
1763 	const auto currentTopLevelQItems = m_topLevelQItems;
1764 	for (QGraphicsItem* qitem : currentTopLevelQItems) {
1765 		if (dynamic_cast<Ball*>(qitem))
1766 		{
1767 			//do not delete balls
1768 			newTopLevelQItems << qitem;
1769 			continue;
1770 		}
1771 		CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
1772 		if (citem)
1773 		{
1774 			delete citem;
1775 		}
1776 	}
1777 
1778 	m_moveableQItems = m_topLevelQItems = newTopLevelQItems;
1779 	selectedItem = nullptr;
1780 
1781 	// will tell basic course info
1782 	// we do this here for the hell of it.
1783 	// there is no fake id, by the way,
1784 	// because it's old and when i added ids i forgot to change it.
1785 	cfgGroup = KConfigGroup(cfg->group(QStringLiteral("0-course@-50,-50")));
1786 	holeInfo.setAuthor(cfgGroup.readEntry("author", holeInfo.author()));
1787 	holeInfo.setName(cfgGroup.readEntry("Name", holeInfo.name()));
1788 	holeInfo.setUntranslatedName(cfgGroup.readEntryUntranslated("Name", holeInfo.untranslatedName()));
1789 	Q_EMIT titleChanged(holeInfo.name());
1790 
1791 	cfgGroup = KConfigGroup(KSharedConfig::openConfig(filename), QStringLiteral("%1-hole@-50,-50|0").arg(curHole));
1792 	curPar = cfgGroup.readEntry("par", 3);
1793 	holeInfo.setPar(curPar);
1794 	holeInfo.borderWallsChanged(cfgGroup.readEntry("borderWalls", holeInfo.borderWalls()));
1795 	holeInfo.setMaxStrokes(cfgGroup.readEntry("maxstrokes", 10));
1796 
1797 	QStringList missingPlugins;
1798 
1799 	// The "for" loop depends on the list of groups being in sorted order.
1800 	QStringList groups = cfg->groupList();
1801 	groups.sort();
1802 
1803 	int numItems = 0;
1804 	int _highestHole = 0;
1805 
1806 	for (QStringList::const_iterator it = groups.constBegin(); it != groups.constEnd(); ++it)
1807 	{
1808 		// Format of group name is [<holeNum>-<name>@<x>,<y>|<id>]
1809 		cfgGroup = KConfigGroup(cfg->group(*it));
1810 
1811 		const int len = (*it).length();
1812 		const int dashIndex = (*it).indexOf(QLatin1Char('-'));
1813 		const int holeNum = (*it).leftRef(dashIndex).toInt();
1814 		if (holeNum > _highestHole)
1815 			_highestHole = holeNum;
1816 
1817 		const int atIndex = (*it).indexOf(QLatin1Char('@'));
1818 		const QString name = (*it).mid(dashIndex + 1, atIndex - (dashIndex + 1));
1819 
1820 		if (holeNum != curHole)
1821 		{
1822 			// Break before reading all groups, if the highest hole
1823 			// number is known and all items in curHole are done.
1824 			if (numItems && !recalcHighestHole)
1825 				break;
1826 			continue;
1827 		}
1828 		numItems++;
1829 
1830 
1831 		const int commaIndex = (*it).indexOf(QLatin1Char(','));
1832 		const int pipeIndex = (*it).indexOf(QLatin1Char('|'));
1833 		const int x = (*it).midRef(atIndex + 1, commaIndex - (atIndex + 1)).toInt();
1834 		const int y = (*it).midRef(commaIndex + 1, pipeIndex - (commaIndex + 1)).toInt();
1835 
1836 		// will tell where ball is
1837 		if (name == QLatin1String("ball"))
1838 		{
1839 			for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1840 				(*it).ball()->setPos(x, y);
1841 			whiteBall->setPos(x, y);
1842 			continue;
1843 		}
1844 
1845 		const int id = (*it).rightRef(len - (pipeIndex + 1)).toInt();
1846 
1847 		QGraphicsItem* newItem = m_factory.createInstance(name, courseBoard, g_world);
1848 		if (newItem)
1849 		{
1850 			m_topLevelQItems << newItem;
1851 			m_moveableQItems << newItem;
1852 			CanvasItem *sceneItem = dynamic_cast<CanvasItem *>(newItem);
1853 
1854 			if (!sceneItem)
1855 				continue;
1856 
1857 			sceneItem->setId(id);
1858 			sceneItem->setGame(this);
1859 			sceneItem->editModeChanged(editing);
1860 			sceneItem->setName(name);
1861 			m_moveableQItems.append(sceneItem->moveableItems());
1862 
1863 			sceneItem->setPosition(QPointF(x, y));
1864 			newItem->setVisible(true);
1865 
1866 			// make things actually show
1867 			cfgGroup = KConfigGroup(cfg->group(makeGroup(id, curHole, sceneItem->name(), x, y)));
1868 			sceneItem->load(&cfgGroup);
1869 		}
1870 		else if (name != QLatin1String("hole") && !missingPlugins.contains(name))
1871 			missingPlugins.append(name);
1872 
1873 	}
1874 
1875 	if (!missingPlugins.empty())
1876 	{
1877 		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)));
1878 	}
1879 
1880 	lastDelId = -1;
1881 
1882 	// if it's the first hole let's not
1883 	if (!numItems && curHole > 1 && !addingNewHole && curHole >= _highestHole)
1884 	{
1885 		// we're done, let's quit
1886 		curHole--;
1887 		pause();
1888 		Q_EMIT holesDone();
1889 
1890 		// tidy things up
1891 		setBorderWalls(false);
1892 		clearHole();
1893 		setModified(false);
1894 		for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
1895 			(*it).ball()->setVisible(false);
1896 
1897 		return;
1898 	}
1899 
1900 	// do it down here; if !hasFinalLoad, do it up there!
1901 	//QGraphicsItem *qsceneItem = 0;
1902 	QList<QGraphicsItem *>::const_iterator qsceneItem;
1903 	QList<CanvasItem *> todo;
1904 	QList<QGraphicsItem *> qtodo;
1905 
1906 	if (curHole > _highestHole)
1907 		_highestHole = curHole;
1908 
1909 	if (recalcHighestHole)
1910 	{
1911 		highestHole = _highestHole;
1912 		recalcHighestHole = false;
1913 		Q_EMIT largestHole(highestHole);
1914 	}
1915 
1916 	if (curHole == 1 && !filename.isNull() && !infoShown)
1917 	{
1918 		// let's not now, because they see it when they choose course
1919 		//showInfoDlg(true);
1920 		infoShown = true;
1921 	}
1922 
1923 	setModified(false);
1924 }
1925 
addNewObject(const QString & identifier)1926 void KolfGame::addNewObject(const QString& identifier)
1927 {
1928 	QGraphicsItem *newItem = m_factory.createInstance(identifier, courseBoard, g_world);
1929 
1930 	m_topLevelQItems << newItem;
1931 	m_moveableQItems << newItem;
1932 	if(!newItem->isVisible())
1933 		newItem->setVisible(true);
1934 
1935 	CanvasItem *sceneItem = dynamic_cast<CanvasItem *>(newItem);
1936 	if (!sceneItem)
1937 		return;
1938 
1939 	// we need to find a number that isn't taken
1940 	int i = lastDelId > 0? lastDelId : m_topLevelQItems.count() - 30;
1941 	if (i <= 0)
1942 		i = 0;
1943 
1944 	for (;; ++i)
1945 	{
1946 		bool found = false;
1947 		for (QGraphicsItem* qitem : std::as_const(m_topLevelQItems)) {
1948 			CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
1949 			if (citem)
1950 			{
1951 				if (citem->curId() == i)
1952 				{
1953 					found = true;
1954 					break;
1955 				}
1956 			}
1957 		}
1958 
1959 
1960 		if (!found)
1961 			break;
1962 	}
1963 	sceneItem->setId(i);
1964 
1965 	sceneItem->setGame(this);
1966 
1967 	const auto infoItems = sceneItem->infoItems();
1968 	for (QGraphicsItem* qitem : infoItems)
1969 		qitem->setVisible(m_showInfo);
1970 
1971 	sceneItem->editModeChanged(editing);
1972 
1973 	sceneItem->setName(identifier);
1974 	m_moveableQItems.append(sceneItem->moveableItems());
1975 
1976 	newItem->setPos(width/2 - 18, height / 2 - 18);
1977 	sceneItem->moveBy(0, 0);
1978 	sceneItem->setSize(newItem->boundingRect().size());
1979 
1980 	setModified(true);
1981 }
1982 
askSave(bool noMoreChances)1983 bool KolfGame::askSave(bool noMoreChances)
1984 {
1985 	if (!modified)
1986 		// not cancel, don't save
1987 		return false;
1988 
1989 	int result = KMessageBox::warningYesNoCancel(this, i18n("There are unsaved changes to current hole. Save them?"), i18n("Unsaved Changes"), KStandardGuiItem::save(), noMoreChances? KStandardGuiItem::discard() : KGuiItem(i18n("Save &Later")), KStandardGuiItem::cancel(), noMoreChances? QStringLiteral("DiscardAsk") : QStringLiteral("SaveAsk"));
1990 	switch (result)
1991 	{
1992 		case KMessageBox::Yes:
1993 			save();
1994 			// fallthrough
1995 
1996 		case KMessageBox::No:
1997 			return false;
1998 			break;
1999 
2000 		case KMessageBox::Cancel:
2001 			return true;
2002 			break;
2003 
2004 		default:
2005 			break;
2006 	}
2007 
2008 	return false;
2009 }
2010 
addNewHole()2011 void KolfGame::addNewHole()
2012 {
2013 	if (askSave(true))
2014 		return;
2015 
2016 	// either it's already false
2017 	// because it was saved by askSave(),
2018 	// or the user pressed the 'discard' button
2019 	setModified(false);
2020 
2021 	// find highest hole num, and create new hole
2022 	// now openFile makes highest hole for us
2023 
2024 	addingNewHole = true;
2025 	curHole = highestHole;
2026 	recalcHighestHole = true;
2027 	startNextHole();
2028 	addingNewHole = false;
2029 	Q_EMIT currentHole(curHole);
2030 
2031 	// make sure even the current player isn't showing
2032 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
2033 		(*it).ball()->setVisible(false);
2034 
2035 	whiteBall->setVisible(editing);
2036 	putter->setVisible(!editing);
2037 	inPlay = false;
2038 
2039 	// add default objects
2040 	const auto knownTypes = m_factory.knownTypes();
2041 	for (const Kolf::ItemMetadata& metadata : knownTypes)
2042 		if (metadata.addOnNewHole)
2043 			addNewObject(metadata.identifier);
2044 
2045 	save();
2046 }
2047 
2048 // kantan deshou ;-)
resetHole()2049 void KolfGame::resetHole()
2050 {
2051 	if (askSave(true))
2052 		return;
2053 	setModified(false);
2054 	curHole--;
2055 	startNextHole();
2056 	resetHoleScores();
2057 }
2058 
resetHoleScores()2059 void KolfGame::resetHoleScores()
2060 {
2061 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
2062 	{
2063 		(*it).resetScore(curHole);
2064 		Q_EMIT scoreChanged((*it).id(), curHole, 0);
2065 	}
2066 }
2067 
clearHole()2068 void KolfGame::clearHole()
2069 {
2070 	QList<QGraphicsItem*> newTopLevelQItems;
2071 	const auto currentTopLevelQItems = m_topLevelQItems;
2072 	for (QGraphicsItem* qitem : currentTopLevelQItems) {
2073 		if (dynamic_cast<Ball*>(qitem))
2074 		{
2075 			//do not delete balls
2076 			newTopLevelQItems << qitem;
2077 			continue;
2078 		}
2079 		CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
2080 		if (citem)
2081 		{
2082 			delete citem;
2083 		}
2084 	}
2085 
2086 	m_moveableQItems = m_topLevelQItems = newTopLevelQItems;
2087 	setSelectedItem(nullptr);
2088 
2089 	// add default objects
2090 	const auto knownTypes = m_factory.knownTypes();
2091 	for (const Kolf::ItemMetadata& metadata : knownTypes)
2092 		if (metadata.addOnNewHole)
2093 			addNewObject(metadata.identifier);
2094 
2095 	setModified(true);
2096 }
2097 
switchHole(int hole)2098 void KolfGame::switchHole(int hole)
2099 {
2100 	if (inPlay)
2101 		return;
2102 	if (hole < 1 || hole > highestHole)
2103 		return;
2104 
2105 	bool wasEditing = editing;
2106 	if (editing)
2107 		toggleEditMode();
2108 
2109 	if (askSave(true))
2110 		return;
2111 	setModified(false);
2112 
2113 	curHole = hole;
2114 	resetHole();
2115 
2116 	if (wasEditing)
2117 		toggleEditMode();
2118 }
2119 
switchHole(const QString & holestring)2120 void KolfGame::switchHole(const QString &holestring)
2121 {
2122 	bool ok;
2123 	int hole = holestring.toInt(&ok);
2124 	if (!ok)
2125 		return;
2126 	switchHole(hole);
2127 }
2128 
nextHole()2129 void KolfGame::nextHole()
2130 {
2131 	switchHole(curHole + 1);
2132 }
2133 
prevHole()2134 void KolfGame::prevHole()
2135 {
2136 	switchHole(curHole - 1);
2137 }
2138 
firstHole()2139 void KolfGame::firstHole()
2140 {
2141 	switchHole(1);
2142 }
2143 
lastHole()2144 void KolfGame::lastHole()
2145 {
2146 	switchHole(highestHole);
2147 }
2148 
randHole()2149 void KolfGame::randHole()
2150 {
2151 	const int newHole = QRandomGenerator::global()->bounded(1, highestHole);
2152 	switchHole(newHole);
2153 }
2154 
save()2155 void KolfGame::save()
2156 {
2157 	if (filename.isEmpty())
2158 	{
2159 		QPointer<QFileDialog> fileSaveDialog = new QFileDialog(this);
2160 		fileSaveDialog->setWindowTitle(i18nc("@title:window", "Pick Kolf Course to Save To"));
2161 		fileSaveDialog->setMimeTypeFilters(QStringList(QStringLiteral("application/x-kourse")));
2162 		fileSaveDialog->setAcceptMode(QFileDialog::AcceptSave);
2163 		if (fileSaveDialog->exec() == QDialog::Accepted) {
2164 			QUrl newfile = fileSaveDialog->selectedUrls().first();
2165 			if (newfile.isEmpty()) {
2166 				return;
2167 			}
2168 			else {
2169 				setFilename(newfile.toLocalFile());
2170 			}
2171 		}
2172 		delete fileSaveDialog;
2173 	}
2174 
2175 	Q_EMIT parChanged(curHole, holeInfo.par());
2176 	Q_EMIT titleChanged(holeInfo.name());
2177 
2178 	const QStringList groups = cfg->groupList();
2179 
2180 	// wipe out all groups from this hole
2181 	for (QStringList::const_iterator it = groups.begin(); it != groups.end(); ++it)
2182 	{
2183 		int holeNum = (*it).leftRef((*it).indexOf(QLatin1Char('-'))).toInt();
2184 		if (holeNum == curHole)
2185 			cfg->deleteGroup(*it);
2186 	}
2187 	for (QGraphicsItem* qitem : std::as_const(m_topLevelQItems)) {
2188 		CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
2189 		if (citem)
2190 		{
2191 			cfgGroup = KConfigGroup(cfg->group(makeGroup(citem->curId(), curHole, citem->name(), (int)qitem->x(), (int)qitem->y())));
2192 			citem->save(&cfgGroup);
2193 		}
2194 	}
2195 
2196 	// save where ball starts (whiteBall tells all)
2197 	cfgGroup = KConfigGroup(cfg->group(QStringLiteral("%1-ball@%2,%3").arg(curHole).arg((int)whiteBall->x()).arg((int)whiteBall->y())));
2198 	cfgGroup.writeEntry("dummykey", true);
2199 
2200 	cfgGroup = KConfigGroup(cfg->group(QStringLiteral("0-course@-50,-50")));
2201 	cfgGroup.writeEntry("author", holeInfo.author());
2202 	cfgGroup.writeEntry("Name", holeInfo.untranslatedName());
2203 
2204 	// save hole info
2205 	cfgGroup = KConfigGroup(cfg->group(QStringLiteral("%1-hole@-50,-50|0").arg(curHole)));
2206 	cfgGroup.writeEntry("par", holeInfo.par());
2207 	cfgGroup.writeEntry("maxstrokes", holeInfo.maxStrokes());
2208 	cfgGroup.writeEntry("borderWalls", holeInfo.borderWalls());
2209 
2210 	cfg->sync();
2211 
2212 	setModified(false);
2213 }
2214 
toggleEditMode()2215 void KolfGame::toggleEditMode()
2216 {
2217 	// won't be editing anymore, and user wants to cancel, we return
2218 	// this is pretty useless. when the person leaves the hole,
2219 	// he gets asked again
2220 	/*
2221 	   if (editing && modified)
2222 	   {
2223 	   if (askSave(false))
2224 	   {
2225 	   Q_EMIT checkEditing();
2226 	   return;
2227 	   }
2228 	   }
2229 	   */
2230 
2231 	selectedItem = nullptr;
2232 
2233 	editing = !editing;
2234 
2235 	if (editing)
2236 	{
2237 		Q_EMIT editingStarted();
2238 		setSelectedItem(nullptr);
2239 	}
2240 	else
2241 	{
2242 		Q_EMIT editingEnded();
2243 		setCursor(Qt::ArrowCursor);
2244 	}
2245 
2246 	// alert our items
2247 	for (QGraphicsItem* qitem : std::as_const(m_topLevelQItems)) {
2248 		if (dynamic_cast<Ball*>(qitem)) continue;
2249 		CanvasItem *citem = dynamic_cast<CanvasItem *>(qitem);
2250 		if (citem)
2251 			citem->editModeChanged(editing);
2252 	}
2253 
2254 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
2255 	{
2256 		// curplayer shouldn't be hidden no matter what
2257 		if ((*it).ball()->beginningOfHole() && it != curPlayer)
2258 			(*it).ball()->setVisible(false);
2259 		else
2260 			(*it).ball()->setVisible(!editing);
2261 	}
2262 
2263 	whiteBall->setVisible(editing);
2264 	whiteBall->editModeChanged(editing);
2265 
2266 	// shouldn't see putter whilst editing
2267 	putter->setVisible(!editing);
2268 
2269 	if (editing)
2270 		autoSaveTimer->start(autoSaveMsec);
2271 	else
2272 		autoSaveTimer->stop();
2273 
2274 	inPlay = false;
2275 }
2276 
setSelectedItem(CanvasItem * citem)2277 void KolfGame::setSelectedItem(CanvasItem* citem)
2278 {
2279 	QGraphicsItem* qitem = dynamic_cast<QGraphicsItem*>(citem);
2280 	selectedItem = qitem;
2281 	Q_EMIT newSelectedItem(qitem ? citem : &holeInfo);
2282 	//deactivate all other overlays
2283 	for (QGraphicsItem* otherQitem : std::as_const(m_topLevelQItems)) {
2284 		CanvasItem* otherCitem = dynamic_cast<CanvasItem*>(otherQitem);
2285 		if (otherCitem && otherCitem != citem)
2286 		{
2287 			//false = do not create overlay if it does not exist yet
2288 			Kolf::Overlay* otherOverlay = otherCitem->overlay(false);
2289 			if (otherOverlay)
2290 				otherOverlay->setState(Kolf::Overlay::Passive);
2291 		}
2292 	}
2293 }
2294 
borderWallsChanged(bool yes)2295 void HoleInfo::borderWallsChanged(bool yes)
2296 {
2297 	m_borderWalls = yes;
2298 	game->setBorderWalls(yes);
2299 }
2300 
allPlayersDone()2301 bool KolfGame::allPlayersDone()
2302 {
2303 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
2304 		if ((*it).ball()->curState() != Holed)
2305 			return false;
2306 
2307 	return true;
2308 }
2309 
setBorderWalls(bool showing)2310 void KolfGame::setBorderWalls(bool showing)
2311 {
2312 	for (Kolf::Wall* wall : std::as_const(borderWalls))
2313 		wall->setVisible(showing);
2314 }
2315 
setUseAdvancedPutting(bool yes)2316 void KolfGame::setUseAdvancedPutting(bool yes)
2317 {
2318 	m_useAdvancedPutting = yes;
2319 
2320 	// increase maxStrength in advanced putting mode
2321 	if (yes)
2322 		maxStrength = 65;
2323 	else
2324 		maxStrength = 55;
2325 }
2326 
setShowGuideLine(bool yes)2327 void KolfGame::setShowGuideLine(bool yes)
2328 {
2329 	putter->setShowGuideLine(yes);
2330 }
2331 
setSound(bool yes)2332 void KolfGame::setSound(bool yes)
2333 {
2334 	m_sound = yes;
2335 }
2336 
courseInfo(CourseInfo & info,const QString & filename)2337 void KolfGame::courseInfo(CourseInfo &info, const QString& filename)
2338 {
2339 	KConfig config(filename);
2340 	KConfigGroup configGroup (config.group(QStringLiteral("0-course@-50,-50")));
2341 	info.author = configGroup.readEntry("author", info.author);
2342 	info.name = configGroup.readEntry("Name", configGroup.readEntry("name", info.name));
2343 	info.untranslatedName = configGroup.readEntryUntranslated("Name", configGroup.readEntryUntranslated("name", info.name));
2344 
2345 	unsigned int hole = 1;
2346 	unsigned int par= 0;
2347 	while (1)
2348 	{
2349 		QString group = QStringLiteral("%1-hole@-50,-50|0").arg(hole);
2350 		if (!config.hasGroup(group))
2351 		{
2352 			hole--;
2353 			break;
2354 		}
2355 
2356 		configGroup = KConfigGroup(config.group(group));
2357 		par += configGroup.readEntry("par", 3);
2358 
2359 		hole++;
2360 	}
2361 
2362 	info.par = par;
2363 	info.holes = hole;
2364 }
2365 
scoresFromSaved(KConfig * config,PlayerList & players)2366 void KolfGame::scoresFromSaved(KConfig *config, PlayerList &players)
2367 {
2368 	KConfigGroup configGroup(config->group(QStringLiteral("0 Saved Game")));
2369 	int numPlayers = configGroup.readEntry("Players", 0);
2370 	if (numPlayers <= 0)
2371 		return;
2372 
2373 	for (int i = 1; i <= numPlayers; ++i)
2374 	{
2375 		// this is same as in kolf.cpp, but we use saved game values
2376 		configGroup = KConfigGroup(config->group(QString::number(i)));
2377 		players.append(Player());
2378 		players.last().ball()->setColor(configGroup.readEntry("Color", "#ffffff"));
2379 		players.last().setName(configGroup.readEntry("Name"));
2380 		players.last().setId(i);
2381 
2382 		const QStringList scores(configGroup.readEntry("Scores",QStringList()));
2383 		QList<int> intscores;
2384 		for (QStringList::const_iterator it = scores.begin(); it != scores.end(); ++it)
2385 			intscores.append((*it).toInt());
2386 
2387 		players.last().setScores(intscores);
2388 	}
2389 }
2390 
saveScores(KConfig * config)2391 void KolfGame::saveScores(KConfig *config)
2392 {
2393 	// wipe out old player info
2394 	const QStringList groups = config->groupList();
2395 	for (QStringList::const_iterator it = groups.begin(); it != groups.end(); ++it)
2396 	{
2397 		// this deletes all int groups, ie, the player info groups
2398 		bool ok = false;
2399 		(*it).toInt(&ok);
2400 		if (ok)
2401 			config->deleteGroup(*it);
2402 	}
2403 
2404 	KConfigGroup configGroup(config->group(QStringLiteral("0 Saved Game")));
2405 	configGroup.writeEntry("Players", players->count());
2406 	configGroup.writeEntry("Course", filename);
2407 	configGroup.writeEntry("Current Hole", curHole);
2408 
2409 	for (PlayerList::Iterator it = players->begin(); it != players->end(); ++it)
2410 	{
2411 		KConfigGroup configGroup(config->group(QString::number((*it).id())));
2412 		configGroup.writeEntry("Name", (*it).name());
2413 		configGroup.writeEntry("Color", (*it).ball()->color().name());
2414 
2415 		QStringList scores;
2416 		QList<int> intscores = (*it).scores();
2417 		for (QList<int>::Iterator it = intscores.begin(); it != intscores.end(); ++it)
2418 			scores.append(QString::number(*it));
2419 
2420 		configGroup.writeEntry("Scores", scores);
2421 	}
2422 }
2423 
CourseInfo()2424 CourseInfo::CourseInfo()
2425 	: name(i18n("Course Name")), author(i18n("Course Author")), holes(0), par(0)
2426 {
2427 }
2428 
2429 
2430