1/*
2 * Copyright 2007-2009 Parker Coates <coates@kde.org>
3 *
4 * This file is part of Killbots.
5 *
6 * Killbots is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * Killbots is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with Killbots. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include "scene.h"
21
22#include "numericdisplayitem.h"
23#include "renderer.h"
24#include "settings.h"
25
26#include <KGamePopupItem>
27
28#include <KDE/KDebug>
29
30#include <QtGui/QPainter>
31#include <QtGui/QGraphicsSceneMouseEvent>
32#include <QtGui/QGraphicsView>
33
34#include <cmath>
35
36
37Killbots::Scene::Scene( QObject * parent )
38 : QGraphicsScene( parent ),
39 m_hero( 0 ),
40 m_rows( 0 ),
41 m_columns( 0 )
42{
43 setItemIndexMethod( QGraphicsScene::NoIndex );
44}
45
46
47Killbots::Scene::~Scene()
48{
49}
50
51
52void Killbots::Scene::addNumericDisplay( NumericDisplayItem * displayItem )
53{
54 addItem( displayItem );
55 displayItem->setPos( -1000000, 0 );
56 m_numericDisplays << displayItem;
57}
58
59
60void Killbots::Scene::setGridSize(int rows, int columns)
61{
62 if ( m_rows != rows || m_columns != columns )
63 {
64 m_rows = rows;
65 m_columns = columns;
66 }
67}
68
69
70void Killbots::Scene::forgetHero()
71{
72 m_hero = 0;
73}
74
75
76Killbots::Sprite * Killbots::Scene::createSprite( SpriteType type, QPoint position )
77{
78 Sprite * sprite = new Sprite();
79 sprite->setSpriteType( type );
80 sprite->setRenderSize( m_cellSize );
81 sprite->enqueueGridPos( position );
82 updateSpritePos( sprite, position );
83 sprite->scale( 0, 0 );
84 // A bit of a hack, but we use the sprite type for stacking order.
85 sprite->setZValue( type );
86
87
88 addItem( sprite );
89
90 if ( type == Hero )
91 m_hero = sprite;
92
93 return sprite;
94}
95
96
97void Killbots::Scene::animateSprites( const QList<Sprite *> & newSprites,
98 const QList<Sprite *> & slidingSprites,
99 const QList<Sprite *> & teleportingSprites,
100 const QList<Sprite *> & destroyedSprites,
101 qreal value
102 ) const
103{
104 static bool halfDone = false;
105
106 if ( value == 0.0 )
107 {
108 halfDone = false;
109 }
110 else if ( value < 1.0 )
111 {
112 foreach ( Sprite * sprite, newSprites )
113 {
114 sprite->resetTransform();
115 sprite->scale( value, value );
116 }
117
118 foreach ( Sprite * sprite, slidingSprites )
119 {
120 QPointF posInGridCoordinates = value * QPointF( sprite->nextGridPos() - sprite->currentGridPos() ) + sprite->currentGridPos();
121 sprite->setPos( QPointF( posInGridCoordinates.x() * m_cellSize.width(), posInGridCoordinates.y() * m_cellSize.height() ) );
122 }
123
124 qreal scaleFactor = value < 0.5
125 ? 1.0 - 2 * value
126 : 2 * value - 1.0;
127
128 if ( !halfDone && value >= 0.5 )
129 {
130 halfDone = true;
131 foreach ( Sprite * sprite, teleportingSprites )
132 updateSpritePos( sprite, sprite->nextGridPos() );
133 }
134
135 foreach ( Sprite * sprite, teleportingSprites )
136 {
137 sprite->resetTransform();
138 sprite->scale( scaleFactor, scaleFactor );
139 }
140
141 foreach ( Sprite * sprite, destroyedSprites )
142 {
143 sprite->resetTransform();
144 sprite->scale( 1 - value, 1 - value );
145 sprite->rotate( value * 180 );
146 }
147 }
148 else
149 {
150 foreach ( Sprite * sprite, newSprites + slidingSprites + teleportingSprites )
151 {
152 sprite->resetTransform();
153 sprite->advanceGridPosQueue();
154 updateSpritePos( sprite, sprite->currentGridPos() );
155 }
156
157 qDeleteAll( destroyedSprites );
158 }
159}
160
161
162void Killbots::Scene::doLayout()
163{
164 QSize size = views().first()->size();
165
166 // If no game has been started
167 if ( m_rows == 0 || m_columns == 0 )
168 {
169 setSceneRect( QRectF( QPointF( 0, 0 ), size ) );
170 return;
171 }
172
173 kDebug() << "Laying out scene at" << size;
174
175 // Make certain layout properties proportional to the scene height,
176 // but clamp them between reasonable values. There's probably room for more
177 // tweaking here.
178 const int baseDimension = qMin( size.width(), size.height() ) / 35;
179 const int spacing = qBound( 5, baseDimension, 15 );
180 const int newFontPixelSize = qBound( QFontInfo( QFont() ).pixelSize(), baseDimension, 25 );
181 const qreal aspectRatio = Renderer::self()->aspectRatio();
182
183 QSize displaySize;
184 // If the font size has changed, resize all the displays (visible or not).
185 if ( m_numericDisplays.first()->font().pixelSize() != newFontPixelSize )
186 {
187 QFont font;
188 font.setPixelSize( newFontPixelSize );
189
190 foreach ( NumericDisplayItem * display, m_numericDisplays )
191 {
192 display->setFont( font );
193 displaySize = displaySize.expandedTo( display->preferredSize() );
194 }
195 foreach ( NumericDisplayItem * display, m_numericDisplays )
196 display->setRenderSize( displaySize );
197 }
198 else
199 {
200 displaySize = m_numericDisplays.first()->boundingRect().size().toSize();
201 }
202
203 // The rest of the function deals only with a list of visible displays.
204 QList<NumericDisplayItem *> visibleDisplays;
205 foreach ( NumericDisplayItem * display, m_numericDisplays )
206 {
207 if ( display->isVisible() )
208 visibleDisplays << display;
209 }
210
211 // Determine the total width required to arrange the displays horizontally.
212 const int widthOfDisplaysOnTop = visibleDisplays.size() * displaySize.width()
213 + ( visibleDisplays.size() - 1 ) * spacing;
214
215 // The displays can either be placed centred, across the top of the
216 // scene or top-aligned, down the side of the scene. We first calculate
217 // what the cell size would be for both options.
218 int availableWidth = size.width() - 3 * spacing - displaySize.width();
219 int availableHeight = size.height() - 2 * spacing;
220 const qreal cellWidthSide = ( availableWidth / m_columns < availableHeight / m_rows * aspectRatio )
221 ? availableWidth / m_columns
222 : availableHeight / m_rows * aspectRatio;
223
224 availableWidth = size.width() - 2 * spacing;
225 availableHeight = size.height() - 3 * spacing - displaySize.height();
226 const qreal cellWidthTop = ( availableWidth / m_columns < availableHeight / m_rows * aspectRatio )
227 ? availableWidth / m_columns
228 : availableHeight / m_rows * aspectRatio;
229
230 // If placing the displays on top would result in larger cells, we take
231 // that option, but only if the displays would actually fit.
232 const bool displaysOnTop = ( cellWidthTop > cellWidthSide && size.width() > widthOfDisplaysOnTop );
233 const qreal newCellWidth = displaysOnTop ? cellWidthTop : cellWidthSide;
234 m_cellSize = QSize( qRound( newCellWidth ), qRound( newCellWidth / aspectRatio ) );
235
236 foreach ( QGraphicsItem * item, items() )
237 {
238 Sprite * sprite = qgraphicsitem_cast<Sprite *>( item );
239 if ( sprite )
240 {
241 sprite->setRenderSize( m_cellSize );
242 updateSpritePos( sprite, sprite->currentGridPos() );
243 }
244 }
245
246 if ( displaysOnTop )
247 {
248 // Set the sceneRect to centre the grid if possible, but ensure the display items are visible
249 const qreal sceneRectXPos = -( size.width() - m_cellSize.width() * ( m_columns - 1 ) ) / 2.0;
250 const qreal centeredYPos = - ( size.height() - m_cellSize.height() * ( m_rows - 1 ) ) / 2.0;
251 const qreal indentedYPos = - ( m_cellSize.height() / 2.0 + 2 * spacing + displaySize.height() );
252 const qreal sceneRectYPos = qMin( centeredYPos, indentedYPos );
253
254 // Position the display items centered at the top of the scene
255 const qreal displayYPos = ( sceneRectYPos - ( displaySize.height() + m_cellSize.height() / 2.0 ) ) / 2;
256
257 int xPos = sceneRectXPos + ( size.width() - widthOfDisplaysOnTop ) / 2.0;
258 foreach ( NumericDisplayItem * display, visibleDisplays )
259 {
260 display->setPos( xPos, displayYPos );
261 xPos += displaySize.width() + spacing;
262 }
263
264 setSceneRect( QRectF( sceneRectXPos, sceneRectYPos, size.width(), size.height() ) );
265 }
266 else
267 {
268 qreal sceneRectXPos;
269 const qreal centeredXPos = - ( size.width() - m_cellSize.width() * ( m_columns - 1 ) ) / 2.0;
270 const qreal sceneRectYPos = -( size.height() - m_cellSize.height() * ( m_rows - 1 ) ) / 2.0;
271 qreal displayXPos;
272
273 // If the application layout is LTR, place the displays on left,
274 // otherwise, place them on the right.
275 if ( views().first()->layoutDirection() == Qt::LeftToRight )
276 {
277 // Set the sceneRect to centre the grid if possible, but ensure the display items are visible
278 const qreal indentedXPos = - ( m_cellSize.width() / 2.0 + 2 * spacing + displaySize.width() );
279 sceneRectXPos = qMin( centeredXPos, indentedXPos );
280
281 // Position the display items to the left of the grid
282 displayXPos = - ( spacing + displaySize.width() + m_cellSize.width() / 2 );
283 }
284 else
285 {
286 // Set the sceneRect to centre the grid if possible, but ensure the display items are visible
287 const qreal indentedXPos = ( m_cellSize.width() * m_columns + 1 * spacing + displaySize.width() ) - size.width();
288 sceneRectXPos = qMax( centeredXPos, indentedXPos );
289
290 // Position the display items to the right of the grid
291 displayXPos = m_cellSize.width() * ( m_columns - 0.5 ) + spacing;
292 }
293
294 int yPos = -m_cellSize.height() / 2;
295 foreach ( NumericDisplayItem * display, visibleDisplays )
296 {
297 display->setPos( displayXPos, yPos );
298 yPos += displaySize.height() + spacing;
299 }
300
301 setSceneRect( QRectF( sceneRectXPos, sceneRectYPos, size.width(), size.height() ) );
302 }
303
304 // Update the scene background
305 QPainter p;
306 QRect gridRect( -sceneRect().x() - m_cellSize.width() / 2,
307 -sceneRect().y() - m_cellSize.height() / 2,
308 m_columns * m_cellSize.width(),
309 m_rows * m_cellSize.height()
310 );
311
312 QPixmap unrotated = Renderer::self()->spritePixmap( "background", size );
313 p.begin( &unrotated );
314 p.drawTiledPixmap( gridRect, Renderer::self()->spritePixmap( "cell", m_cellSize ) );
315 p.end();
316
317 // The background brush begins painting at 0,0 but our sceneRect doesn't
318 // start at 0,0 so we have to "rotate" the pixmap so that it looks right
319 // when tiled.
320 QPixmap background( size );
321 background.fill( Qt::transparent );
322 p.begin( &background );
323 p.drawTiledPixmap( background.rect(), unrotated, -sceneRect().topLeft().toPoint() );
324 p.end();
325
326 setBackgroundBrush( background );
327
328 update();
329}
330
331
332void Killbots::Scene::mouseMoveEvent( QGraphicsSceneMouseEvent * event )
333{
334 getMouseDirection( event->scenePos() );
335 QGraphicsScene::mouseMoveEvent( event );
336}
337
338
339void Killbots::Scene::mouseReleaseEvent( QGraphicsSceneMouseEvent * event )
340{
341 HeroAction actionFromPosition = getMouseDirection( event->scenePos() );
342
343 if ( actionFromPosition != NoAction )
344 {
345 Settings::ClickAction userAction = Settings::Nothing;
346
347 if ( event->button() == Qt::LeftButton )
348 {
349 if ( event->modifiers() & Qt::ControlModifier )
350 userAction = Settings::middleClickAction();
351 else
352 userAction = Settings::Step;
353 }
354 else if ( event->button() == Qt::RightButton )
355 userAction = Settings::rightClickAction();
356 else if ( event->button() == Qt::MidButton )
357 userAction = Settings::middleClickAction();
358
359 if ( userAction == Settings::Step )
360 emit clicked( actionFromPosition );
361 else if ( userAction == Settings::RepeatedStep )
362 emit clicked( -actionFromPosition - 1 );
363 else if ( userAction == Settings::Teleport )
364 emit clicked( Teleport );
365 else if ( userAction == Settings::TeleportSafely )
366 emit clicked( TeleportSafely );
367 else if ( userAction == Settings::TeleportSafelyIfPossible )
368 emit clicked( TeleportSafelyIfPossible );
369 else if ( userAction == Settings::WaitOutRound )
370 emit clicked( WaitOutRound );
371 }
372
373 QGraphicsScene::mouseReleaseEvent( event );
374}
375
376
377Killbots::HeroAction Killbots::Scene::getMouseDirection( QPointF cursorPosition ) const
378{
379 HeroAction result;
380 const bool heroOnScreen = m_hero && sceneRect().contains( m_hero->sceneBoundingRect() );
381
382 if ( heroOnScreen && !popupAtPosition( cursorPosition ) )
383 {
384 if ( m_hero->sceneBoundingRect().contains( cursorPosition ) )
385 result = Hold;
386 else
387 {
388 const qreal piOver4 = 0.78539816339744830961566L;
389
390 QPointF delta = cursorPosition - m_hero->sceneBoundingRect().center();
391 int direction = qRound( atan2( -delta.y(), delta.x() ) / piOver4 );
392 if ( direction < 0 )
393 direction += 8;
394
395 result = static_cast<HeroAction>( direction );
396 }
397
398 views().first()->setCursor( Renderer::self()->cursorFromAction( result ) );
399 }
400 else
401 {
402 views().first()->unsetCursor();
403 result = NoAction;
404 }
405
406 return result;
407}
408
409
410bool Killbots::Scene::popupAtPosition( QPointF position ) const
411{
412 foreach ( QGraphicsItem * item, items( position ) )
413 {
414 if ( dynamic_cast<KGamePopupItem *>( item ) != 0 )
415 return true;
416 }
417 return false;
418}
419
420
421void Killbots::Scene::updateSpritePos( Sprite * sprite, QPoint gridPosition ) const
422{
423 sprite->setPos( gridPosition.x() * m_cellSize.width(), gridPosition.y() * m_cellSize.height() );
424}
425
426#include "moc_scene.cpp"
427