1 /*
2     Copyright (C) 2002-2005, Jason Katz-Brown <jasonkb@mit.edu>
3     Copyright 2008, 2009, 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 "canvasitem.h"
21 #include "game.h"
22 #include "landscape.h"
23 #include "overlay.h"
24 #include "shape.h"
25 
26 #include <Box2D/Dynamics/b2Body.h>
27 #include <Box2D/Dynamics/b2World.h>
28 
29 //this is how much a strut and the items on it are raised
30 static const int ZValueStep = 100;
31 
CanvasItem(b2World * world)32 CanvasItem::CanvasItem(b2World* world)
33 	: game(nullptr)
34 	, m_zBehavior(CanvasItem::FixedZValue)
35 	, m_zValue(0)
36 	, m_strut(nullptr)
37 	, m_staticStrut(nullptr)
38 	, m_body(nullptr)
39 	, m_overlay(nullptr)
40 	, m_simulationType((CanvasItem::SimulationType) -1)
41 {
42 	b2BodyDef bodyDef;
43 	bodyDef.userData = this;
44 	m_body = world->CreateBody(&bodyDef);
45 	setSimulationType(CanvasItem::CollisionSimulation);
46 }
47 
~CanvasItem()48 CanvasItem::~CanvasItem()
49 {
50 	//disconnect struts
51 	if (m_strut)
52 		m_strut->m_struttedItems.removeAll(this);
53 	for (CanvasItem* item : std::as_const(m_struttedItems))
54 		item->m_strut = nullptr;
55 	//The overlay is deleted first, because it might interact with all other parts of the object.
56 	delete m_overlay;
57 	//NOTE: Box2D objects will need to be destroyed in the following order:
58 	//subobjects, shapes, own b2Body
59 	qDeleteAll(m_shapes);
60 	m_body->GetWorld()->DestroyBody(m_body);
61 }
62 
setZBehavior(CanvasItem::ZBehavior behavior,qreal zValue)63 void CanvasItem::setZBehavior(CanvasItem::ZBehavior behavior, qreal zValue)
64 {
65 	m_zBehavior = behavior;
66 	m_zValue = zValue;
67 	QGraphicsItem* qitem = dynamic_cast<QGraphicsItem*>(this);
68 	if (qitem)
69 	{
70 		if (m_zBehavior == CanvasItem::FixedZValue)
71 			qitem->setZValue(m_zValue);
72 		else
73 			updateZ(qitem);
74 	}
75 }
76 
setStaticStrut(CanvasItem * citem)77 void CanvasItem::setStaticStrut(CanvasItem* citem)
78 {
79 	m_staticStrut = citem;
80 }
81 
updateZ(QGraphicsItem * self)82 void CanvasItem::updateZ(QGraphicsItem* self)
83 {
84 	//disconnect from old strut (if any)
85 	//TODO: not if old strut is new strut (or did I forget some cornercases?)
86 	if (m_strut)
87 	{
88 		m_strut->m_struttedItems.removeAll(this);
89 		m_strut = nullptr;
90 	}
91 	//simple behavior
92 	if (m_zBehavior == CanvasItem::FixedZValue)
93 	{
94 		self->setZValue(m_zValue);
95 		return;
96 	}
97 	if (m_zBehavior == CanvasItem::IsStrut)
98 	{
99 		self->setZValue(ZValueStep);
100 		return;
101 	}
102 	//determine new strut
103 	if (m_staticStrut)
104 		m_strut = m_staticStrut;
105 	else
106 	{
107 		const auto collidingItems = self->collidingItems();
108 		for (QGraphicsItem* qitem :collidingItems) {
109 			CanvasItem* citem = dynamic_cast<CanvasItem*>(qitem);
110 			if (citem && citem->m_zBehavior == CanvasItem::IsStrut)
111 			{
112 				//special condition for slopes: they must lie inside the strut's area, not only touch it
113 				Kolf::Slope* slope = dynamic_cast<Kolf::Slope*>(this);
114 				if (slope)
115 					if (!slope->collidesWithItem(qitem, Qt::ContainsItemBoundingRect))
116 						continue;
117 				//strut found
118 				m_strut = citem;
119 				break;
120 			}
121 		}
122 	}
123 	//strut found?
124 	if (m_strut)
125 	{
126 		m_strut->m_struttedItems << this;
127 		self->setZValue(m_zValue + ZValueStep);
128 	}
129 	//no strut found -> set default zValue
130 	else
131 		self->setZValue(m_zValue);
132 }
133 
moveItemsOnStrut(const QPointF & posDiff)134 void CanvasItem::moveItemsOnStrut(const QPointF& posDiff)
135 {
136 	for (CanvasItem* citem : std::as_const(m_struttedItems)) {
137 		QGraphicsItem* qitem = dynamic_cast<QGraphicsItem*>(citem);
138 		if (!qitem || qitem->data(0) == Rtti_Putter)
139 			continue;
140 		citem->moveBy(posDiff.x(), posDiff.y());
141 		Ball* ball = dynamic_cast<Ball*>(citem);
142 		if (ball && game && !game->isEditing() && game->curBall() == ball)
143 			game->ballMoved();
144 	}
145 }
146 
mayCollide(CanvasItem * citem1,CanvasItem * citem2)147 /*static*/ bool CanvasItem::mayCollide(CanvasItem* citem1, CanvasItem* citem2)
148 {
149 	//which one is the ball?
150 	Ball* ball = dynamic_cast<Ball*>(citem1);
151 	CanvasItem* citem = citem2;
152 	if (!ball)
153 	{
154 		ball = dynamic_cast<Ball*>(citem2);
155 		citem = citem1;
156 	}
157 	if (!ball)
158 		//huh, no ball involved? then don't restrict anything, because
159 		//that likely introduces weird bugs later
160 		return true;
161 	//if both items are graphicsitems, restrict collisions of ball to those
162 	//objects on same strut level or above (i.e. don't collide with
163 	//stuff below the current strut)
164 	const QGraphicsItem* qitem = dynamic_cast<QGraphicsItem*>(citem);
165 	if (!qitem)
166 		return true;
167 	const int ballStrutLevel = int(ball->zValue()) / ZValueStep;
168 	const int itemStrutLevel = int(qitem->zValue()) / ZValueStep;
169 	return ballStrutLevel <= itemStrutLevel;
170 }
171 
moveBy(double dx,double dy)172 void CanvasItem::moveBy(double dx, double dy)
173 {
174 	Q_UNUSED(dx) Q_UNUSED(dy)
175 	QGraphicsItem* qitem = dynamic_cast<QGraphicsItem*>(this);
176 	if (qitem)
177 		updateZ(qitem);
178 }
179 
save(KConfigGroup * cfgGroup)180 void CanvasItem::save(KConfigGroup *cfgGroup)
181 {
182 	cfgGroup->writeEntry("dummykey", true);
183 }
184 
editModeChanged(bool editing)185 void CanvasItem::editModeChanged(bool editing)
186 {
187 	Kolf::Overlay* overlay = this->overlay();
188 	if (overlay)
189 		overlay->setVisible(editing);
190 }
191 
world() const192 b2World* CanvasItem::world() const
193 {
194 	return m_body->GetWorld();
195 }
196 
addShape(Kolf::Shape * shape)197 void CanvasItem::addShape(Kolf::Shape* shape)
198 {
199 	if (shape->attach(this)) //this will fail if the shape is already attached to some object
200 		m_shapes << shape;
201 }
202 
setSimulationType(CanvasItem::SimulationType type)203 void CanvasItem::setSimulationType(CanvasItem::SimulationType type)
204 {
205 	if (m_simulationType != type)
206 	{
207 		m_simulationType = type;
208 		//write type into b2Body
209 		b2BodyType b2type; bool b2active;
210 		switch (type)
211 		{
212 			case CanvasItem::NoSimulation:
213 				b2type = b2_staticBody;
214 				b2active = false;
215 				break;
216 			case CanvasItem::CollisionSimulation:
217 				b2type = b2_staticBody;
218 				b2active = true;
219 				break;
220 			case CanvasItem::KinematicSimulation:
221 				b2type = b2_kinematicBody;
222 				b2active = true;
223 				break;
224 			case CanvasItem::DynamicSimulation: default:
225 				b2type = b2_dynamicBody;
226 				b2active = true;
227 				break;
228 		}
229 		m_body->SetType(b2type);
230 		m_body->SetActive(b2active);
231 	}
232 }
233 
velocity() const234 QPointF CanvasItem::velocity() const
235 {
236 	b2Vec2 v = m_body->GetLinearVelocity();
237 	return QPointF(v.x, v.y);
238 }
239 
setVelocity(const QPointF & newVelocity)240 void CanvasItem::setVelocity(const QPointF& newVelocity)
241 {
242 	const QPointF currentVelocity = this->velocity();
243 	if (newVelocity != currentVelocity)
244 	{
245 		const qreal mass = m_body->GetMass();
246 		//WARNING: Velocities are NOT scaled. The timestep is scaled, instead.
247 		//See where b2World::Step() gets called for more info.
248 		if (mass == 0 || m_simulationType != CanvasItem::DynamicSimulation)
249 		{
250 			m_body->SetLinearVelocity(b2Vec2(newVelocity.x(), newVelocity.y()));
251 		}
252 		else
253 		{
254 			const QPointF impulse = (newVelocity - currentVelocity) * mass;
255 			m_body->ApplyLinearImpulse(b2Vec2(impulse.x(), impulse.y()), m_body->GetPosition());
256 		}
257 	}
258 }
259 
startSimulation()260 void CanvasItem::startSimulation()
261 {
262 	const QPointF position = getPosition() * Kolf::Box2DScaleFactor;
263 	m_body->SetTransform(b2Vec2(position.x(), position.y()), 0);
264 }
265 
endSimulation()266 void CanvasItem::endSimulation()
267 {
268 	//read position
269 	b2Vec2 v = m_body->GetPosition();
270 	QPointF position = QPointF(v.x, v.y) / Kolf::Box2DScaleFactor;
271 	if (position != getPosition())
272 		//HACK: The above condition can be removed later, but for now we need to
273 		//prevent moveBy() from being called with (0, 0) arguments because such
274 		//have a non-standard behavior with some classes (e.g. Ball), i.e. these
275 		//arguments trigger some black magic
276 		setPosition(position);
277 }
278 
overlay(bool createIfNecessary)279 Kolf::Overlay* CanvasItem::overlay(bool createIfNecessary)
280 {
281 	//the overlay is created once it is requested
282 	if (!m_overlay && createIfNecessary)
283 	{
284 		m_overlay = createOverlay();
285 		if (m_overlay)
286 		{
287 			//should be above object representation
288 			m_overlay->setZValue(m_overlay->qitem()->zValue() + 100);
289 			//initialize the overlay's parameters
290 			m_overlay->update();
291 		}
292 	}
293 	return m_overlay;
294 }
295 
propagateUpdate()296 void CanvasItem::propagateUpdate()
297 {
298 	if (m_overlay)
299 		m_overlay->update();
300 }
301 
302 //BEGIN EllipticalCanvasItem
303 
EllipticalCanvasItem(bool withEllipse,const QString & spriteKey,QGraphicsItem * parent,b2World * world)304 EllipticalCanvasItem::EllipticalCanvasItem(bool withEllipse, const QString& spriteKey, QGraphicsItem* parent, b2World* world)
305 	: Tagaro::SpriteObjectItem(Kolf::renderer(), spriteKey, parent)
306 	, CanvasItem(world)
307 	, m_ellipseItem(nullptr)
308 	, m_shape(nullptr)
309 {
310 	if (withEllipse)
311 	{
312 		m_ellipseItem = new QGraphicsEllipseItem(this);
313 		m_ellipseItem->setFlag(QGraphicsItem::ItemStacksBehindParent);
314 		//won't appear unless pen/brush is configured
315 		m_ellipseItem->setPen(Qt::NoPen);
316 		m_ellipseItem->setBrush(Qt::NoBrush);
317 	}
318 	m_shape = new Kolf::EllipseShape(QRectF());
319 	addShape(m_shape);
320 }
321 
contains(const QPointF & point) const322 bool EllipticalCanvasItem::contains(const QPointF& point) const
323 {
324 	const QSizeF halfSize = size() / 2;
325 	const qreal xScaled = point.x() / halfSize.width();
326 	const qreal yScaled = point.y() / halfSize.height();
327 	return xScaled * xScaled + yScaled * yScaled < 1;
328 }
329 
shape() const330 QPainterPath EllipticalCanvasItem::shape() const
331 {
332 	QPainterPath path;
333 	path.addEllipse(rect());
334 	return path;
335 }
336 
rect() const337 QRectF EllipticalCanvasItem::rect() const
338 {
339 	return Tagaro::SpriteObjectItem::boundingRect();
340 }
341 
setSize(const QSizeF & size)342 void EllipticalCanvasItem::setSize(const QSizeF& size)
343 {
344 	setOffset(QPointF(-0.5 * size.width(), -0.5 * size.height()));
345 	Tagaro::SpriteObjectItem::setSize(size);
346 	if (m_ellipseItem)
347 		m_ellipseItem->setRect(this->rect());
348 	m_shape->setRect(this->rect());
349 }
350 
moveBy(double dx,double dy)351 void EllipticalCanvasItem::moveBy(double dx, double dy)
352 {
353 	Tagaro::SpriteObjectItem::moveBy(dx, dy);
354 	CanvasItem::moveBy(dx, dy);
355 }
356 
saveSize(KConfigGroup * group)357 void EllipticalCanvasItem::saveSize(KConfigGroup* group)
358 {
359 	const QSizeF size = this->size();
360 	group->writeEntry("width", size.width());
361 	group->writeEntry("height", size.height());
362 }
363 
loadSize(KConfigGroup * group)364 void EllipticalCanvasItem::loadSize(KConfigGroup* group)
365 {
366 	QSizeF size = this->size();
367 	size.rwidth() = group->readEntry("width", size.width());
368 	size.rheight() = group->readEntry("height", size.height());
369 	setSize(size);
370 }
371 
372 //END EllipticalCanvasItem
373 //BEGIN ArrowItem
374 
ArrowItem(QGraphicsItem * parent)375 ArrowItem::ArrowItem(QGraphicsItem* parent)
376 	: QGraphicsPathItem(parent)
377 	, m_angle(0), m_length(20)
378 	, m_reversed(false)
379 {
380 	updatePath();
381 	setPen(QPen(Qt::black));
382 	setBrush(Qt::NoBrush);
383 }
384 
angle() const385 qreal ArrowItem::angle() const
386 {
387 	return m_angle;
388 }
389 
setAngle(qreal angle)390 void ArrowItem::setAngle(qreal angle)
391 {
392 	if (m_angle != angle)
393 	{
394 		m_angle = angle;
395 		updatePath();
396 	}
397 }
398 
length() const399 qreal ArrowItem::length() const
400 {
401 	return m_length;
402 }
403 
setLength(qreal length)404 void ArrowItem::setLength(qreal length)
405 {
406 	if (m_length != length)
407 	{
408 		m_length = qMax<qreal>(length, 0.0);
409 		updatePath();
410 	}
411 }
412 
isReversed() const413 bool ArrowItem::isReversed() const
414 {
415 	return m_reversed;
416 }
417 
setReversed(bool reversed)418 void ArrowItem::setReversed(bool reversed)
419 {
420 	if (m_reversed != reversed)
421 	{
422 		m_reversed = reversed;
423 		updatePath();
424 	}
425 }
426 
vector() const427 Vector ArrowItem::vector() const
428 {
429 	return Vector::fromMagnitudeDirection(m_length, m_angle);
430 }
431 
updatePath()432 void ArrowItem::updatePath()
433 {
434 	if (m_length == 0)
435 	{
436 		setPath(QPainterPath());
437 		return;
438 	}
439 	//the following three points define the arrow tip
440 	const QPointF extent = Vector::fromMagnitudeDirection(m_length, m_angle);
441 	const QPointF startPoint = m_reversed ? extent : QPointF();
442 	const QPointF endPoint = m_reversed ? QPointF() : extent;
443 	const QPointF point1 = endPoint - Vector::fromMagnitudeDirection(m_length / 2, m_angle + M_PI / 12);
444 	const QPointF point2 = endPoint - Vector::fromMagnitudeDirection(m_length / 2, m_angle - M_PI / 12);
445 	QPainterPath path;
446 	path.addPolygon(QPolygonF() << startPoint << endPoint);
447 	path.addPolygon(QPolygonF() << point1 << endPoint << point2);
448 	setPath(path);
449 }
450 
451 //END ArrowItem
452