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 "landscape.h"
21#include "ball.h"
22#include "game.h"
23
24#include <QBoxLayout>
25#include <QCheckBox>
26#include <QLabel>
27#include <QSlider>
28#include <KComboBox>
29#include <KConfigGroup>
30#include <KGlobal>
31#include <KLocale>
32#include <KNumInput>
33
34//BEGIN Kolf::LandscapeItem
35//END Kolf::LandscapeItem
36
37Kolf::LandscapeItem::LandscapeItem(const QString& type, QGraphicsItem* parent, b2World* world)
38 : EllipticalCanvasItem(false, type, parent, world)
39 , m_blinkEnabled(false)
40 , m_blinkInterval(50)
41 , m_blinkFrame(0)
42{
43 setSimulationType(CanvasItem::NoSimulation);
44}
45
46bool Kolf::LandscapeItem::isBlinkEnabled() const
47{
48 return m_blinkEnabled;
49}
50
51void Kolf::LandscapeItem::setBlinkEnabled(bool blinkEnabled)
52{
53 m_blinkEnabled = blinkEnabled;
54 //reset animation
55 m_blinkFrame = 0;
56 setVisible(true);
57}
58
59int Kolf::LandscapeItem::blinkInterval() const
60{
61 return m_blinkInterval;
62}
63
64void Kolf::LandscapeItem::setBlinkInterval(int blinkInterval)
65{
66 m_blinkInterval = blinkInterval;
67 //reset animation
68 m_blinkFrame = 0;
69 setVisible(true);
70}
71
72void Kolf::LandscapeItem::advance(int phase)
73{
74 EllipticalCanvasItem::advance(phase);
75 if (phase == 1 && m_blinkEnabled)
76 {
77 const int actualInterval = 1.8 * (10 + m_blinkInterval);
78 m_blinkFrame = (m_blinkFrame + 1) % (2 * actualInterval);
79 setVisible(m_blinkFrame < actualInterval);
80 }
81}
82
83void Kolf::LandscapeItem::load(KConfigGroup* group)
84{
85 EllipticalCanvasItem::loadSize(group);
86 setBlinkEnabled(group->readEntry("changeEnabled", m_blinkEnabled));
87 setBlinkInterval(group->readEntry("changeEvery", m_blinkInterval));
88}
89
90void Kolf::LandscapeItem::save(KConfigGroup* group)
91{
92 EllipticalCanvasItem::saveSize(group);
93 group->writeEntry("changeEnabled", m_blinkEnabled);
94 group->writeEntry("changeEvery", m_blinkInterval);
95}
96
97Config* Kolf::LandscapeItem::config(QWidget* parent)
98{
99 return new Kolf::LandscapeConfig(this, parent);
100}
101
102Kolf::Overlay* Kolf::LandscapeItem::createOverlay()
103{
104 return new Kolf::LandscapeOverlay(this);
105}
106
107//BEGIN Kolf::LandscapeOverlay
108
109Kolf::LandscapeOverlay::LandscapeOverlay(Kolf::LandscapeItem* item)
110 : Kolf::Overlay(item, item)
111{
112 //TODO: code duplication to Kolf::RectangleOverlay and Kolf::SlopeOverlay
113 for (int i = 0; i < 4; ++i)
114 {
115 Kolf::OverlayHandle* handle = new Kolf::OverlayHandle(Kolf::OverlayHandle::CircleShape, this);
116 m_handles << handle;
117 addHandle(handle);
118 connect(handle, SIGNAL(moveRequest(QPointF)), this, SLOT(moveHandle(QPointF)));
119 }
120}
121
122void Kolf::LandscapeOverlay::update()
123{
124 Kolf::Overlay::update();
125 const QRectF rect = qitem()->boundingRect();
126 m_handles[0]->setPos(rect.topLeft());
127 m_handles[1]->setPos(rect.topRight());
128 m_handles[2]->setPos(rect.bottomLeft());
129 m_handles[3]->setPos(rect.bottomRight());
130}
131
132void Kolf::LandscapeOverlay::moveHandle(const QPointF& handleScenePos)
133{
134 const QPointF handlePos = mapFromScene(handleScenePos);
135 //factor 2: item bounding rect is always centered around (0,0)
136 QSizeF newSize(2 * qAbs(handlePos.x()), 2 * qAbs(handlePos.y()));
137 dynamic_cast<Kolf::LandscapeItem*>(qitem())->setSize(newSize);
138}
139
140//END Kolf::LandscapeOverlay
141//BEGIN Kolf::LandscapeConfig
142
143Kolf::LandscapeConfig::LandscapeConfig(Kolf::LandscapeItem* item, QWidget* parent)
144 : Config(parent)
145{
146 QVBoxLayout* vlayout = new QVBoxLayout(this);
147 QCheckBox* checkBox = new QCheckBox(i18n("Enable show/hide"), this);
148 vlayout->addWidget(checkBox);
149
150 QHBoxLayout* hlayout = new QHBoxLayout;
151 vlayout->addLayout(hlayout);
152 QLabel* label1 = new QLabel(i18n("Slow"), this);
153 hlayout->addWidget(label1);
154 QSlider* slider = new QSlider(Qt::Horizontal, this);
155 hlayout->addWidget(slider);
156 QLabel* label2 = new QLabel(i18n("Fast"), this);
157 hlayout->addWidget(label2);
158
159 vlayout->addStretch();
160
161 checkBox->setChecked(true);
162 connect(checkBox, SIGNAL(toggled(bool)), label1, SLOT(setEnabled(bool)));
163 connect(checkBox, SIGNAL(toggled(bool)), label2, SLOT(setEnabled(bool)));
164 connect(checkBox, SIGNAL(toggled(bool)), slider, SLOT(setEnabled(bool)));
165 connect(checkBox, SIGNAL(toggled(bool)), item, SLOT(setBlinkEnabled(bool)));
166 checkBox->setChecked(item->isBlinkEnabled());
167 slider->setRange(1, 100);
168 slider->setPageStep(5);
169 slider->setValue(100 - item->blinkInterval());
170 connect(slider, SIGNAL(valueChanged(int)), SLOT(setBlinkInterval(int)));
171 connect(this, SIGNAL(blinkIntervalChanged(int)), item, SLOT(setBlinkInterval(int)));
172}
173
174void Kolf::LandscapeConfig::setBlinkInterval(int sliderValue)
175{
176 emit blinkIntervalChanged(100 - sliderValue);
177}
178
179//END Kolf::LandscapeConfig
180//BEGIN Kolf::Puddle
181
182Kolf::Puddle::Puddle(QGraphicsItem* parent, b2World* world)
183 : Kolf::LandscapeItem(QLatin1String("puddle"), parent, world)
184{
185 setData(0, Rtti_DontPlaceOn);
186 setSize(QSizeF(45, 30));
187 setZBehavior(CanvasItem::FixedZValue, 3);
188}
189
190bool Kolf::Puddle::collision(Ball* ball)
191{
192 if (!ball->isVisible())
193 return false;
194 if (!contains(ball->pos() - pos()))
195 return true;
196 //ball is visible and has reached the puddle
197 playSound("puddle");
198 ball->setAddStroke(ball->addStroke() + 1);
199 ball->setPlaceOnGround(true);
200 ball->setVisible(false);
201 ball->setState(Stopped);
202 ball->setVelocity(Vector());
203 if (game && game->curBall() == ball)
204 game->stoppedBall();
205 return false;
206}
207
208//END Kolf::Puddle
209//BEGIN Kolf::Sand
210
211Kolf::Sand::Sand(QGraphicsItem* parent, b2World* world)
212 : Kolf::LandscapeItem(QLatin1String("sand"), parent, world)
213{
214 setSize(QSizeF(45, 40));
215 setZBehavior(CanvasItem::FixedZValue, 2);
216}
217
218bool Kolf::Sand::collision(Ball* ball)
219{
220 if (contains(ball->pos() - pos()))
221 ball->setFrictionMultiplier(7);
222 return true;
223}
224
225//END Kolf::Sand
226//BEGIN Kolf::Slope
227
228struct SlopeData
229{
230 QStringList gradientKeys, translatedGradientKeys;
231 QStringList spriteKeys, reversedSpriteKeys;
232 SlopeData()
233 {
234 gradientKeys << QLatin1String("Vertical")
235 << QLatin1String("Horizontal")
236 << QLatin1String("Diagonal")
237 << QLatin1String("Opposite Diagonal")
238 << QLatin1String("Elliptic");
239 translatedGradientKeys << i18n("Vertical")
240 << i18n("Horizontal")
241 << i18n("Diagonal")
242 << i18n("Opposite Diagonal")
243 << i18n("Elliptic");
244 spriteKeys << QLatin1String("slope_n")
245 << QLatin1String("slope_w")
246 << QLatin1String("slope_nw")
247 << QLatin1String("slope_ne")
248 << QLatin1String("slope_bump");
249 reversedSpriteKeys << QLatin1String("slope_s")
250 << QLatin1String("slope_e")
251 << QLatin1String("slope_se")
252 << QLatin1String("slope_sw")
253 << QLatin1String("slope_dip");
254 }
255};
256K_GLOBAL_STATIC(SlopeData, g_slopeData)
257
258Kolf::Slope::Slope(QGraphicsItem* parent, b2World* world)
259 : Tagaro::SpriteObjectItem(Kolf::renderer(), QString(), parent)
260 , CanvasItem(world)
261 , m_grade(4)
262 , m_reversed(false)
263 , m_stuckOnGround(false)
264 , m_type(Kolf::VerticalSlope)
265 , m_gradeItem(new QGraphicsSimpleTextItem(this))
266{
267 m_gradeItem->setBrush(Qt::white);
268 m_gradeItem->setVisible(false);
269 m_gradeItem->setZValue(1);
270 for (int i = 0; i < 4; ++i)
271 {
272 ArrowItem* arrow = new ArrowItem(this);
273 arrow->setLength(0);
274 arrow->setVisible(false);
275 m_arrows << arrow;
276 }
277 setSize(QSizeF(40, 40));
278 m_stuckOnGround = true; //so that the following call does not return early
279 setStuckOnGround(false); //initializes Z behavior
280 updateAppearance();
281}
282
283double Kolf::Slope::grade() const
284{
285 return m_grade;
286}
287
288void Kolf::Slope::setGrade(double grade)
289{
290 if (m_grade != grade && grade > 0)
291 {
292 m_grade = grade;
293 updateAppearance();
294 propagateUpdate();
295 }
296}
297
298bool Kolf::Slope::isReversed() const
299{
300 return m_reversed;
301}
302
303void Kolf::Slope::setReversed(bool reversed)
304{
305 if (m_reversed != reversed)
306 {
307 m_reversed = reversed;
308 updateAppearance();
309 propagateUpdate();
310 }
311}
312
313Kolf::SlopeType Kolf::Slope::slopeType() const
314{
315 return m_type;
316}
317
318void Kolf::Slope::setSlopeType(int type)
319{
320 if (m_type != type && type >= 0)
321 {
322 m_type = (Kolf::SlopeType) type;
323 updateAppearance();
324 propagateUpdate();
325 }
326}
327
328bool Kolf::Slope::isStuckOnGround() const
329{
330 return m_stuckOnGround;
331}
332
333void Kolf::Slope::setStuckOnGround(bool stuckOnGround)
334{
335 if (m_stuckOnGround != stuckOnGround)
336 {
337 m_stuckOnGround = stuckOnGround;
338 setZBehavior(m_stuckOnGround ? CanvasItem::FixedZValue : CanvasItem::IsRaisedByStrut, 1);
339 propagateUpdate();
340 }
341}
342
343QPainterPath Kolf::Slope::shape() const
344{
345 const QRectF rect = boundingRect();
346 QPainterPath path;
347 if (m_type == Kolf::CrossDiagonalSlope) {
348 QPolygonF polygon(3);
349 polygon[0] = rect.topLeft();
350 polygon[1] = rect.bottomRight();
351 polygon[2] = m_reversed ? rect.topRight() : rect.bottomLeft();
352 path.addPolygon(polygon);
353 } else if (m_type == Kolf::DiagonalSlope) {
354 QPolygonF polygon(3);
355 polygon[0] = rect.topRight();
356 polygon[1] = rect.bottomLeft();
357 polygon[2] = m_reversed ? rect.topLeft() : rect.bottomRight();
358 path.addPolygon(polygon);
359 } else if (m_type == Kolf::EllipticSlope) {
360 path.addEllipse(rect);
361 } else {
362 path.addRect(rect);
363 }
364 return path;
365}
366
367void Kolf::Slope::setSize(const QSizeF& size)
368{
369 if (m_type == Kolf::EllipticSlope)
370 {
371 const double extent = qMin(size.width(), size.height());
372 Tagaro::SpriteObjectItem::setSize(extent, extent);
373 }
374 else
375 Tagaro::SpriteObjectItem::setSize(size);
376 updateInfo();
377 propagateUpdate();
378 updateZ(this);
379}
380
381QPointF Kolf::Slope::getPosition() const
382{
383 return Tagaro::SpriteObjectItem::pos();
384}
385
386void Kolf::Slope::moveBy(double dx, double dy)
387{
388 Tagaro::SpriteObjectItem::moveBy(dx, dy);
389 CanvasItem::moveBy(dx, dy);
390}
391
392void Kolf::Slope::load(KConfigGroup* group)
393{
394 setGrade(group->readEntry("grade", m_grade));
395 setReversed(group->readEntry("reversed", m_reversed));
396 setStuckOnGround(group->readEntry("stuckOnGround", m_stuckOnGround));
397 //gradient is a bit more complicated
398 const QString type = group->readEntry("gradient", g_slopeData->gradientKeys.value(m_type));
399 setSlopeType(g_slopeData->gradientKeys.indexOf(type));
400 //read size
401 QSizeF size = Tagaro::SpriteObjectItem::size();
402 size.setWidth(group->readEntry("width", size.width()));
403 size.setHeight(group->readEntry("height", size.height()));
404 setSize(size);
405}
406
407void Kolf::Slope::save(KConfigGroup* group)
408{
409 group->writeEntry("grade", m_grade);
410 group->writeEntry("reversed", m_reversed);
411 group->writeEntry("stuckOnGround", m_stuckOnGround);
412 group->writeEntry("gradient", g_slopeData->gradientKeys.value(m_type));
413 const QSizeF size = Tagaro::SpriteObjectItem::size();
414 group->writeEntry("width", size.width());
415 group->writeEntry("height", size.height());
416}
417
418void Kolf::Slope::updateAppearance()
419{
420 updateInfo();
421 //set pixmap
422 setSpriteKey((m_reversed ? g_slopeData->reversedSpriteKeys : g_slopeData->spriteKeys).value(m_type));
423}
424
425void Kolf::Slope::updateInfo()
426{
427 m_gradeItem->setText(QString::number(m_grade));
428 const QPointF textOffset = m_gradeItem->boundingRect().center();
429 //update arrows
430 const QSizeF size = Tagaro::SpriteObjectItem::size();
431 const double width = size.width(), height = size.height();
432 const double length = sqrt(width * width + height * height) / 4;
433 if (m_type == Kolf::EllipticSlope)
434 {
435 double angle = 0;
436 for (int i = 0; i < 4; ++i, angle += M_PI / 2)
437 {
438 ArrowItem* arrow = m_arrows[i];
439 arrow->setLength(length);
440 arrow->setAngle(angle);
441 arrow->setReversed(m_reversed);
442 arrow->setPos(QPointF(width / 2, height / 2));
443 }
444 m_gradeItem->setPos(QPointF(width / 2, height / 2) - textOffset);
445 }
446 else
447 {
448 double angle = 0;
449 double x = .5 * width, y = .5 * height;
450 switch ((int) m_type)
451 {
452 case Kolf::HorizontalSlope:
453 angle = 0;
454 break;
455 case Kolf::VerticalSlope:
456 angle = M_PI / 2;
457 break;
458 case Kolf::DiagonalSlope:
459 angle = atan(width / height);
460 x = m_reversed ? .25 * width : .75 * width;
461 y = m_reversed ? .25 * height : .75 * height;
462 break;
463 case Kolf::CrossDiagonalSlope:
464 angle = M_PI - atan(width / height);
465 x = m_reversed ? .75 * width : .25 * width;
466 y = m_reversed ? .25 * height : .75 * height;
467 break;
468 }
469 //only one arrow needed - hide all others
470 for (int i = 1; i < 4; ++i)
471 m_arrows[i]->setLength(0);
472 ArrowItem* arrow = m_arrows[0];
473 arrow->setLength(length);
474 arrow->setAngle(m_reversed ? angle : angle + M_PI);
475 arrow->setPos(QPointF(x, y));
476 m_gradeItem->setPos(QPointF(x, y) - textOffset);
477 }
478}
479
480bool Kolf::Slope::collision(Ball* ball)
481{
482 Vector v = ball->velocity();
483 double addto = 0.013 * m_grade;
484
485 const bool diag = m_type == Kolf::DiagonalSlope || m_type == Kolf::CrossDiagonalSlope;
486 const bool circle = m_type == Kolf::EllipticSlope;
487
488 double slopeAngle = 0;
489 const double width = size().width(), height = size().height();
490
491 if (diag)
492 slopeAngle = atan(width / height);
493 else if (circle)
494 {
495 const QPointF start = pos() + QPointF(width, height) / 2.0;
496 const QPointF end = ball->pos();
497
498 Vector betweenVector = start - end;
499 const double factor = betweenVector.magnitude() / (width / 2.0);
500 slopeAngle = betweenVector.direction();
501
502 // this little bit by Daniel
503 addto *= factor * M_PI / 2;
504 addto = sin(addto);
505 }
506
507 if (!m_reversed)
508 addto = -addto;
509 switch ((int) m_type)
510 {
511 case Kolf::HorizontalSlope:
512 v.rx() += addto;
513 break;
514 case Kolf::VerticalSlope:
515 v.ry() += addto;
516 break;
517 case Kolf::DiagonalSlope:
518 case Kolf::EllipticSlope:
519 v.rx() += cos(slopeAngle) * addto;
520 v.ry() += sin(slopeAngle) * addto;
521 break;
522 case Kolf::CrossDiagonalSlope:
523 v.rx() -= cos(slopeAngle) * addto;
524 v.ry() += sin(slopeAngle) * addto;
525 break;
526 }
527 ball->setVelocity(v);
528 ball->setState(v.isNull() ? Stopped : Rolling);
529 // do NOT do terrain collidingItems
530 return false;
531}
532
533bool Kolf::Slope::terrainCollisions() const
534{
535 return true;
536}
537
538QList<QGraphicsItem*> Kolf::Slope::infoItems() const
539{
540 QList<QGraphicsItem*> result;
541 foreach (ArrowItem* arrow, m_arrows)
542 result << arrow;
543 result << m_gradeItem;
544 return result;
545}
546
547Config* Kolf::Slope::config(QWidget* parent)
548{
549 return new Kolf::SlopeConfig(this, parent);
550}
551
552Kolf::Overlay* Kolf::Slope::createOverlay()
553{
554 return new Kolf::SlopeOverlay(this);
555}
556
557//END Kolf::Slope
558//BEGIN Kolf::SlopeConfig
559
560Kolf::SlopeConfig::SlopeConfig(Kolf::Slope* slope, QWidget* parent)
561 : Config(parent)
562{
563 QGridLayout* layout = new QGridLayout(this);
564
565 KComboBox* typeBox = new KComboBox(this);
566 typeBox->addItems(g_slopeData->translatedGradientKeys);
567 typeBox->setCurrentIndex(slope->slopeType());
568 connect(typeBox, SIGNAL(currentIndexChanged(int)), slope, SLOT(setSlopeType(int)));
569 layout->addWidget(typeBox, 0, 0, 1, 2);
570
571 QCheckBox* reversed = new QCheckBox(i18n("Reverse direction"), this);
572 reversed->setChecked(slope->isReversed());
573 connect(reversed, SIGNAL(toggled(bool)), slope, SLOT(setReversed(bool)));
574 layout->addWidget(reversed, 1, 0);
575
576 QCheckBox* stuck = new QCheckBox(i18n("Unmovable"), this);
577 stuck->setChecked(slope->isStuckOnGround());
578 stuck->setWhatsThis(i18n("Whether or not this slope can be moved by other objects, like floaters."));
579 connect(stuck, SIGNAL(toggled(bool)), slope, SLOT(setStuckOnGround(bool)));
580 layout->addWidget(stuck, 1, 1);
581
582 layout->addWidget(new QLabel(i18n("Grade:"), this), 2, 0);
583
584 KDoubleNumInput* grade = new KDoubleNumInput(this);
585 grade->setRange(0, 8, 1, true);
586 grade->setValue(slope->grade());
587 connect(grade, SIGNAL(valueChanged(double)), slope, SLOT(setGrade(double)));
588 layout->addWidget(grade, 2, 1);
589
590 layout->setRowStretch(4, 10);
591}
592
593//END Kolf::SlopeConfig
594//BEGIN Kolf::SlopeOverlay
595
596Kolf::SlopeOverlay::SlopeOverlay(Kolf::Slope* slope)
597 : Kolf::Overlay(slope, slope, true) //true = add shape to outlines
598{
599 //TODO: code duplication to Kolf::LandscapeOverlay and Kolf::RectangleOverlay
600 for (int i = 0; i < 4; ++i)
601 {
602 Kolf::OverlayHandle* handle = new Kolf::OverlayHandle(Kolf::OverlayHandle::CircleShape, this);
603 m_handles << handle;
604 addHandle(handle);
605 connect(handle, SIGNAL(moveRequest(QPointF)), this, SLOT(moveHandle(QPointF)));
606 }
607}
608
609void Kolf::SlopeOverlay::update()
610{
611 Kolf::Overlay::update();
612 const QRectF rect = qitem()->boundingRect();
613 m_handles[0]->setPos(rect.topLeft());
614 m_handles[1]->setPos(rect.topRight());
615 m_handles[2]->setPos(rect.bottomLeft());
616 m_handles[3]->setPos(rect.bottomRight());
617}
618
619void Kolf::SlopeOverlay::moveHandle(const QPointF& handleScenePos)
620{
621 Kolf::OverlayHandle* handle = qobject_cast<Kolf::OverlayHandle*>(sender());
622 const int handleIndex = m_handles.indexOf(handle);
623 Kolf::Slope* item = dynamic_cast<Kolf::Slope*>(qitem());
624 const QPointF handlePos = mapFromScene(handleScenePos);
625 //modify bounding rect using new handlePos
626 QRectF rect(QPointF(), item->size());
627 if (handleIndex % 2 == 0)
628 rect.setLeft(qMin(handlePos.x(), rect.right()));
629 else
630 rect.setRight(qMax(handlePos.x(), rect.left()));
631 if (handleIndex < 2)
632 rect.setTop(qMin(handlePos.y(), rect.bottom()));
633 else
634 rect.setBottom(qMax(handlePos.y(), rect.top()));
635 item->moveBy(rect.x(), rect.y());
636 item->setSize(rect.size());
637}
638
639//END Kolf::SlopeOverlay
640
641#include "landscape.moc"
642