1 /*
2 SPDX-FileCopyrightText: 2012 Roney Gomes <roney477@gmail.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include <KLocalizedString>
8
9 #include <QFont>
10 #include <QTimeLine>
11
12 #include "kgoldrunner_debug.h"
13 #include "kgrview.h"
14 #include "kgrscene.h"
15 #include "kgrsprite.h"
16 #include "kgrrenderer.h"
17
18 const StartFrame animationStartFrames [nAnimationTypes] = {
19 RIGHTWALK1, LEFTWALK1, RIGHTCLIMB1, LEFTCLIMB1,
20 CLIMB1, CLIMB1, FALL1, FALL2,
21 DIGBRICK1, // Start frame for OPEN_BRICK.
22 DIGBRICK6}; // Start frame for CLOSE_BRICK.
23
KGrScene(KGrView * view)24 KGrScene::KGrScene (KGrView * view)
25 :
26 QGraphicsScene (view),
27 // Allow FIELDWIDTH * FIELDHEIGHT tiles for the KGoldruner level-layouts,
28 // plus 2 more tile widths all around for text areas, frame and spillover
29 // for mouse actions (to avoid accidental clicks affecting the desktop).
30 m_view (view),
31 m_background (nullptr),
32 m_level (1),
33 m_title (nullptr),
34 m_replayMessage (nullptr),
35 m_livesText (nullptr),
36 m_scoreText (nullptr),
37 m_hasHintText (nullptr),
38 m_pauseResumeText (nullptr),
39 m_heroId (0),
40 m_tilesWide (FIELDWIDTH + 2 * 2),
41 m_tilesHigh (FIELDHEIGHT + 2 * 2),
42 m_tileSize (10),
43 m_toolbarTileSize (10),
44 m_themeChanged (true),
45 m_topLeftX (0),
46 m_topLeftY (0),
47 m_mouse (new QCursor()),
48 m_fadingTimeLine (new QTimeLine (1000, this))
49 {
50 setItemIndexMethod(NoIndex);
51
52 m_tiles.fill (nullptr, m_tilesWide * m_tilesHigh);
53 m_tileTypes.fill (FREE, m_tilesWide * m_tilesHigh);
54
55 m_renderer = new KGrRenderer (this);
56
57 m_frame = addRect (0, 0, 100, 100); // Create placeholder for frame.
58 m_frame->setVisible (false);
59
60 m_spotlight = addRect (0, 0, 100, 100); // Create placeholder for spot.
61 m_spotlight->setVisible (false);
62
63 m_title = new QGraphicsSimpleTextItem();
64 addItem (m_title);
65
66 m_replayMessage = new QGraphicsSimpleTextItem();
67 addItem (m_replayMessage);
68 m_replayMessage->setVisible (false); // Visible only in demo/replay.
69
70 m_livesText = new QGraphicsSimpleTextItem();
71 addItem (m_livesText);
72
73 m_scoreText = new QGraphicsSimpleTextItem();
74 addItem (m_scoreText);
75
76 m_hasHintText = new QGraphicsSimpleTextItem();
77 addItem (m_hasHintText);
78
79 m_pauseResumeText = new QGraphicsSimpleTextItem();
80 addItem (m_pauseResumeText);
81
82 m_fadingTimeLine->setEasingCurve(QEasingCurve::OutCurve);
83 m_fadingTimeLine->setUpdateInterval (50);
84 connect(m_fadingTimeLine, &QTimeLine::valueChanged, this, &KGrScene::drawSpotlight);
85 connect(m_fadingTimeLine, &QTimeLine::finished, this, &KGrScene::fadeFinished);
86 }
87
~KGrScene()88 KGrScene::~KGrScene()
89 {
90 delete m_mouse;
91 delete m_fadingTimeLine;
92 }
93
redrawScene()94 void KGrScene::redrawScene ()
95 {
96 //qCDebug(KGOLDRUNNER_LOG) << "REDRAW: m_sizeChanged" << m_sizeChanged << "m_themeChanged" << m_themeChanged;
97 bool redrawToolbar = false;
98 if (m_sizeChanged) {
99 // Calculate what size of tile will fit in the view.
100 QSize size = m_view->size();
101 int tileSize = qMin (size.width() / m_tilesWide,
102 size.height() / m_tilesHigh);
103 m_topLeftX = (size.width() - m_tilesWide * tileSize)/2.0;
104 m_topLeftY = (size.height() - m_tilesHigh * tileSize)/2.0;
105 setSceneRect (0, 0, size.width(), size.height());
106 //qCDebug(KGOLDRUNNER_LOG) << "SIZE" << size << "TL" << m_topLeftX << m_topLeftY << "TILE" << tileSize << "was" << m_tileSize << m_toolbarTileSize;
107
108 // Make the fade-out/fade-in rectangle cover the playing area.
109 m_spotlight->setRect (m_topLeftX + 2 * tileSize - 1,
110 m_topLeftY + 2 * tileSize - 1,
111 (m_tilesWide - 4) * tileSize + 2,
112 (m_tilesHigh - 4) * tileSize + 2);
113 m_maxRadius = (5 * m_spotlight->rect().width() + 4) / 8;
114 m_spotlight->setPen (Qt::NoPen);
115 m_spotlight->setZValue (10);
116 m_spotlight->setVisible (false);
117
118 // Set up the gradient to draw the spotlight (black with a hole in it).
119 QPointF center (width() * 0.5, height() * 0.5);
120 m_gradient.setCenter (center);
121 m_gradient.setFocalPoint (center);
122 m_gradient.setRadius (m_maxRadius);
123 m_gradient.setColorAt (1.00, QColor (0, 0, 0, 255));
124 m_gradient.setColorAt (0.85, QColor (0, 0, 0, 0));
125
126 int index = 0;
127 for (KGameRenderedItem * tile : std::as_const(m_tiles)) {
128 if (tile) {
129 setTile (tile, tileSize, index/m_tilesHigh, index%m_tilesHigh);
130 }
131 index++;
132 }
133 for (KGrSprite * sprite : std::as_const(m_sprites)) {
134 if (sprite) {
135 sprite->changeCoordinateSystem
136 (m_topLeftX, m_topLeftY, tileSize);
137 }
138 }
139
140 if (m_tileSize != tileSize) {
141 // Do not expand the toolbar (in edit mode) until there is room for
142 // it. This avoids a nasty expand-contract-expand-contract loop.
143 m_toolbarTileSize = ((tileSize > m_tileSize) && (m_topLeftY == 0)) ?
144 m_tileSize : tileSize;
145 }
146 // When conditions are right, redraw editing icons, if in edit mode.
147 redrawToolbar = ((m_toolbarTileSize != m_tileSize) &&
148 (m_topLeftY > 0)) ? true : false;
149 m_tileSize = tileSize;
150 m_sizeChanged = false;
151 }
152
153 // Re-draw text, background and frame if either scene-size or theme changes.
154
155 // Resize and draw texts for title, score, lives, hasHint and pauseResume.
156 setTextFont (m_title, 0.6);
157 setTitle (m_title->text());
158 placeTextItems();
159
160 // Resize and draw different backgrounds, depending on the level and theme.
161 loadBackground (m_level);
162
163 if (m_renderer->hasBorder()) {
164 // There are border tiles in the theme, so do not draw a frame.
165 m_frame->setVisible (false);
166 }
167 else {
168 // There are no border tiles, so draw a frame around the board.
169 drawFrame();
170 }
171
172 if (m_themeChanged) {
173 // Fill the scene (and view) with the new background color. Do this
174 // even if the background has no border, to avoid ugly white rectangles
175 // appearing if rendering and painting is momentarily a bit slow.
176 setBackgroundBrush (m_renderer->borderColor());
177
178 // Erase border tiles (if any) and draw new ones, if new theme has them.
179 drawBorder();
180
181 // Redraw all the tiles, except for borders and tiles of type FREE.
182 for (int i = 1; i <= FIELDWIDTH; i++) {
183 for (int j = 1; j <= FIELDHEIGHT; j++) {
184 int index = i * m_tilesHigh + j;
185 paintCell (i, j, m_tileTypes[index]);
186 }
187 }
188
189 // Redraw editing icons if theme changes when in edit mode.
190 redrawToolbar = true;
191 m_themeChanged = false;
192 }
193
194 if (redrawToolbar) {
195 m_toolbarTileSize = m_tileSize; // If game is in edit mode, KGoldrunner
196 Q_EMIT redrawEditToolbar(); // object redraws the editToolbar.
197 }
198 }
199
changeTheme()200 void KGrScene::changeTheme()
201 {
202 m_themeChanged = true;
203 redrawScene();
204 }
205
changeSize()206 void KGrScene::changeSize()
207 {
208 m_sizeChanged = true;
209 redrawScene();
210 }
211
setTitle(const QString & newTitle)212 void KGrScene::setTitle (const QString & newTitle)
213 {
214 if (! m_title) return;
215
216 m_title->setText (newTitle);
217 QRectF r = m_title->boundingRect(); // Centre the title.
218 m_title->setPos ((sceneRect().width() - r.width())/2,
219 m_topLeftY + (m_tileSize - r.height())/2);
220 }
221
setReplayMessage(const QString & msg)222 void KGrScene::setReplayMessage (const QString & msg)
223 {
224 m_replayMessage->setText (msg);
225 }
226
showReplayMessage(bool onOff)227 void KGrScene::showReplayMessage (bool onOff)
228 {
229 m_replayMessage->setVisible (onOff);
230 }
231
placeTextItems()232 void KGrScene::placeTextItems()
233 {
234 setTextFont (m_replayMessage, 0.5);
235 setTextFont (m_livesText, 0.5);
236 setTextFont (m_scoreText, 0.5);
237 setTextFont (m_hasHintText, 0.5);
238 setTextFont (m_pauseResumeText, 0.5);
239
240 QRectF r = m_replayMessage->boundingRect();
241 m_replayMessage->setPos ((sceneRect().width() - r.width())/2,
242 m_topLeftY + 1.4 * m_tileSize - 0.5 * r.height());
243 m_replayMessage->setZValue (10);
244
245 qreal totalWidth = 0.0;
246 r = m_livesText->boundingRect();
247 qreal x = m_topLeftX + 2 * m_tileSize;
248 qreal y = sceneRect().height() - m_topLeftY - (m_tileSize + r.height())/2;
249
250 m_livesText->setPos (x, y);
251 totalWidth += r.width();
252
253 r = m_scoreText->boundingRect();
254 m_scoreText->setPos (x + totalWidth, y);
255 totalWidth += r.width();
256
257 r = m_hasHintText->boundingRect();
258 m_hasHintText->setPos (x + totalWidth, y);
259 totalWidth += r.width();
260
261 r = m_pauseResumeText->boundingRect();
262 m_pauseResumeText->setPos (x + totalWidth, y);
263 totalWidth += r.width();
264
265 qreal spacing = ((m_tilesWide - 4) * m_tileSize - totalWidth) / 3.0;
266 if (spacing < 0.0)
267 return;
268
269 m_scoreText->moveBy (spacing, 0.0);
270 m_hasHintText->moveBy (2.0 * spacing, 0.0);
271 m_pauseResumeText->moveBy (3.0 * spacing, 0.0);
272 }
273
showLives(long lives)274 void KGrScene::showLives (long lives)
275 {
276 if (m_livesText)
277 m_livesText->setText (i18n("Lives: %1", QString::number(lives)
278 .rightJustified(3, QLatin1Char('0'))));
279 }
280
showScore(long score)281 void KGrScene::showScore (long score)
282 {
283 if (m_scoreText)
284 m_scoreText->setText (i18n("Score: %1", QString::number(score)
285 .rightJustified(7, QLatin1Char('0'))));
286 }
287
setHasHintText(const QString & msg)288 void KGrScene::setHasHintText (const QString & msg)
289 {
290 if (m_hasHintText)
291 m_hasHintText->setText (msg);
292 }
293
setPauseResumeText(const QString & msg)294 void KGrScene::setPauseResumeText (const QString & msg)
295 {
296 if (m_pauseResumeText)
297 m_pauseResumeText->setText (msg);
298 }
299
goToBlack()300 void KGrScene::goToBlack()
301 {
302 drawSpotlight (0);
303 }
304
fadeIn(bool inOut)305 void KGrScene::fadeIn (bool inOut)
306 {
307 // For fade-in, inOut = true, circle opens, from 0.0 to 1.0.
308 // For fade-out, inOut = false, circle closes, from 1.0 to 0.0.
309 m_fadingTimeLine->setDirection (inOut ? QTimeLine::Forward
310 : QTimeLine::Backward);
311 m_fadingTimeLine->start();
312 }
313
drawSpotlight(qreal ratio)314 void KGrScene::drawSpotlight (qreal ratio)
315 {
316 if (ratio > 0.99) {
317 m_spotlight->setVisible (false); // End of the close-open cycle.
318 return;
319 }
320 else if (ratio <= 0.01) {
321 m_spotlight->setBrush (Qt::black);
322 }
323 else {
324 m_gradient.setRadius (ratio * m_maxRadius);
325 m_spotlight->setBrush (QBrush (m_gradient));
326 }
327
328 m_spotlight->setVisible (true);
329 }
330
setLevel(unsigned int level)331 void KGrScene::setLevel (unsigned int level)
332 {
333 if (level == m_level) {
334 return;
335 }
336 m_level = level;
337 loadBackground (level); // Load background for level.
338 }
339
loadBackground(const int level)340 void KGrScene::loadBackground (const int level)
341 {
342 // NOTE: The background picture can be the same size as the level-layout (as
343 // in the Egypt theme) OR it can be the same size as the entire viewport.
344 // In this example the background is fitted into the level-layout.
345 m_background = m_renderer->getBackground (level, m_background);
346
347 m_background->setRenderSize (QSize ((m_tilesWide - 4) * m_tileSize,
348 (m_tilesHigh - 4) * m_tileSize));
349 m_background->setPos (m_topLeftX + 2 * m_tileSize,
350 m_topLeftY + 2 * m_tileSize);
351 // Keep the background behind the level layout.
352 m_background->setZValue (-1);
353 }
354
setTile(KGameRenderedItem * tile,const int tileSize,const int i,const int j)355 void KGrScene::setTile (KGameRenderedItem * tile, const int tileSize,
356 const int i, const int j)
357 {
358 tile->setRenderSize (QSize (tileSize, tileSize));
359 tile->setPos (m_topLeftX + (i+1) * tileSize, m_topLeftY + (j+1) * tileSize);
360 }
361
setBorderTile(const QString & spriteKey,const int x,const int y)362 void KGrScene::setBorderTile (const QString &spriteKey, const int x, const int y)
363 {
364 int index = x * m_tilesHigh + y;
365 KGameRenderedItem * t = m_renderer->getBorderItem (spriteKey,
366 m_tiles.at(index));
367 m_tiles[index] = t;
368
369 if (t) {
370 setTile (t, m_tileSize, x, y);
371 }
372 }
373
drawBorder()374 void KGrScene::drawBorder()
375 {
376 // Corners.
377 setBorderTile (QStringLiteral("frame-topleft"), 0, 0);
378 setBorderTile (QStringLiteral("frame-topright"), FIELDWIDTH + 1, 0);
379 setBorderTile (QStringLiteral("frame-bottomleft"), 0, FIELDHEIGHT + 1);
380 setBorderTile (QStringLiteral("frame-bottomright"), FIELDWIDTH + 1, FIELDHEIGHT + 1);
381
382 // Upper side.
383 for (int i = 1; i <= FIELDWIDTH; i++)
384 setBorderTile (QStringLiteral("frame-top"), i, 0);
385
386 // Lower side.
387 for (int i = 1; i <= FIELDWIDTH; i++)
388 setBorderTile (QStringLiteral("frame-bottom"), i, FIELDHEIGHT + 1);
389
390 // Left side.
391 for (int i = 1; i <= FIELDHEIGHT; i++)
392 setBorderTile (QStringLiteral("frame-left"), 0, i);
393
394 // Right side.
395 for (int i = 1; i <= FIELDHEIGHT; i++)
396 setBorderTile (QStringLiteral("frame-right"), FIELDWIDTH + 1, i);
397 }
398
drawFrame()399 void KGrScene::drawFrame()
400 {
401 int w = 0.05 * m_tileSize + 0.5;
402 w = w < 1 ? 1 : w;
403 m_frame->setRect (
404 m_topLeftX + (2 * m_tileSize) - (3 * w),
405 m_topLeftY + (2 * m_tileSize) - (3 * w),
406 FIELDWIDTH * m_tileSize + 6 * w,
407 FIELDHEIGHT * m_tileSize + 6 * w);
408 //qCDebug(KGOLDRUNNER_LOG) << "FRAME WIDTH" << w << "tile size" << m_tileSize << "rectangle" << m_frame->rect();
409 QPen pen = QPen (m_renderer->textColor());
410 pen.setWidth (w);
411 m_frame->setPen (pen);
412 m_frame->setVisible (true);
413 }
414
paintCell(const int i,const int j,const char type)415 void KGrScene::paintCell (const int i, const int j, const char type)
416 {
417 int index = i * m_tilesHigh + j;
418 KGameRenderedItem * t = m_renderer->getTileItem (type, m_tiles.at(index));
419 m_tiles[index] = t;
420 m_tileTypes[index] = type;
421
422 if (t) {
423 setTile (t, m_tileSize, i, j);
424 }
425 }
426
makeSprite(const char type,int i,int j)427 int KGrScene::makeSprite (const char type, int i, int j)
428 {
429 int spriteId;
430 KGrSprite * sprite = m_renderer->getSpriteItem (type, TickTime);
431
432 if (m_sprites.count(nullptr) > 0 &&
433 ((spriteId = m_sprites.lastIndexOf (nullptr)) >= 0)) {
434 // Re-use a slot previously occupied by a transient member of the list.
435 m_sprites[spriteId] = sprite;
436 }
437 else {
438 // Otherwise, add to the end of the list.
439 spriteId = m_sprites.count();
440 m_sprites.append (sprite);
441 }
442
443 int frame1 = animationStartFrames [FALL_L];
444
445 switch (type) {
446 case HERO:
447 m_heroId = spriteId;
448 sprite->setZ (1);
449 break;
450 case ENEMY:
451 sprite->setZ (2);
452 break;
453 case BRICK:
454 frame1 = animationStartFrames [OPEN_BRICK];
455
456 // The hero and enemies must be painted in front of dug bricks.
457 sprite->setZ (0);
458
459 // Erase the brick-image so that animations are visible in all themes.
460 paintCell (i, j, FREE);
461 break;
462 default:
463 break;
464 }
465
466 sprite->setFrame (frame1);
467 sprite->setCoordinateSystem (m_topLeftX, m_topLeftY, m_tileSize);
468 addItem (sprite); // The sprite can be correctly rendered now.
469 sprite->move (i, j, frame1);
470 return spriteId;
471 }
472
animate(bool missed)473 void KGrScene::animate (bool missed)
474 {
475 for (KGrSprite * sprite : std::as_const(m_sprites)) {
476 if (sprite != nullptr) {
477 sprite->animate (missed);
478 }
479 }
480 }
481
startAnimation(const int id,const bool repeating,const int i,const int j,const int time,const Direction dirn,const AnimationType type)482 void KGrScene::startAnimation (const int id, const bool repeating,
483 const int i, const int j, const int time,
484 const Direction dirn, const AnimationType type)
485 {
486 // TODO - Put most of this in helper code, based on theme parameters.
487 int dx = 0;
488 int dy = 0;
489 int frame = animationStartFrames [type];
490 int nFrames = 8;
491 int nFrameChanges = 4;
492
493 switch (dirn) {
494 case RIGHT:
495 dx = +1;
496 break;
497 case LEFT:
498 dx = -1;
499 break;
500 case DOWN:
501 dy = +1;
502 if ((type == FALL_R) || (type == FALL_L)) {
503 nFrames = 1;
504 }
505 else {
506 nFrames = 2;
507 }
508 break;
509 case UP:
510 dy = -1;
511 nFrames = 2;
512 break;
513 case STAND:
514 switch (type) {
515 case OPEN_BRICK:
516 nFrames = 5;
517 break;
518 case CLOSE_BRICK:
519 nFrames = 4;
520 break;
521 default:
522 // Show a standing hero or enemy, using the previous StartFrame.
523 nFrames = 0;
524 break;
525 }
526 break;
527 default:
528 break;
529 }
530
531 // TODO - Generalise nFrameChanges = 4, also the tick time = 20 new sprite.
532 m_sprites.at(id)->setAnimation (repeating, i, j, frame, nFrames, dx, dy,
533 time, nFrameChanges);
534 }
535
gotGold(const int spriteId,const int i,const int j,const bool spriteHasGold,const bool lost)536 void KGrScene::gotGold (const int spriteId, const int i, const int j,
537 const bool spriteHasGold, const bool lost)
538 {
539 // Hide collected gold or show dropped gold, but not if the gold was lost.
540 if (! lost) {
541 paintCell (i, j, (spriteHasGold) ? FREE : NUGGET);
542 }
543
544 // If the rules allow, show whether or not an enemy sprite is carrying gold.
545 if (enemiesShowGold && (m_sprites.at(spriteId)->spriteType() == ENEMY)) {
546 m_sprites.at(spriteId)->setSpriteKey (spriteHasGold ? QStringLiteral("gold_enemy")
547 : QStringLiteral("enemy"));
548 }
549 }
550
showHiddenLadders(const QList<int> & ladders,const int width)551 void KGrScene::showHiddenLadders (const QList<int> & ladders, const int width)
552 {
553 for (const int &offset : ladders) {
554 int i = offset % width;
555 int j = offset / width;
556 paintCell (i, j, LADDER);
557 }
558 }
559
deleteSprite(const int spriteId)560 void KGrScene::deleteSprite (const int spriteId)
561 {
562 QPointF loc = m_sprites.at(spriteId)->currentLoc();
563 bool brick = (m_sprites.at(spriteId)->spriteType() == BRICK);
564
565 delete m_sprites.at(spriteId);
566 m_sprites [spriteId] = nullptr;
567
568 if (brick) {
569 // Dug-brick sprite erased: restore the tile that was at that location.
570 paintCell (loc.x(), loc.y(), BRICK);
571 }
572 }
573
deleteAllSprites()574 void KGrScene::deleteAllSprites()
575 {
576 qDeleteAll(m_sprites);
577 m_sprites.clear();
578 }
579
preRenderSprites()580 void KGrScene::preRenderSprites()
581 {
582 char type[2] = {HERO, ENEMY};
583 for (int t = 0; t < 2; t++) {
584 KGrSprite * sprite = m_renderer->getSpriteItem (type[t], TickTime);
585 sprite->setFrame (1);
586 sprite->setRenderSize (QSize (m_tileSize, m_tileSize));
587 int count = sprite->frameCount();
588
589 // Pre-render all frames of the hero and an enemy, to avoid hiccups in
590 // animation during the first few seconds of KGoldrunner execution.
591 for (int n = 1; n <= count; n++) {
592 sprite->setFrame (n);
593 }
594 delete sprite;
595 }
596 }
597
setMousePos(const int i,const int j)598 void KGrScene::setMousePos (const int i, const int j)
599 {
600 m_mouse->setPos (m_view->mapToGlobal (QPoint (
601 m_topLeftX + (i + 1) * m_tileSize + m_tileSize/2,
602 m_topLeftY + (j + 1) * m_tileSize + m_tileSize/2)));
603 }
604
getMousePos(int & i,int & j)605 void KGrScene::getMousePos (int & i, int & j)
606 {
607 QPoint pos = m_view->mapFromGlobal (m_mouse->pos());
608 i = pos.x();
609 j = pos.y();
610 if (! m_view->isActiveWindow()) {
611 i = -2;
612 j = -2;
613 return;
614 }
615 // IDW TODO - Check for being outside scene. Use saved m_width and m_height.
616
617 i = (i - m_topLeftX)/m_tileSize - 1;
618 j = (j - m_topLeftY)/m_tileSize - 1;
619
620 // Make sure i and j are within the KGoldrunner playing area.
621 i = (i < 1) ? 1 : ((i > FIELDWIDTH) ? FIELDWIDTH : i);
622 j = (j < 1) ? 1 : ((j > FIELDHEIGHT) ? FIELDHEIGHT : j);
623 }
624
setTextFont(QGraphicsSimpleTextItem * t,double fontFraction)625 void KGrScene::setTextFont (QGraphicsSimpleTextItem * t, double fontFraction)
626 {
627 QFont f;
628 f.setPixelSize ((int) (m_tileSize * fontFraction + 0.5));
629 f.setWeight (QFont::Bold);
630 f.setStretch (QFont::Expanded);
631 t->setBrush (m_renderer->textColor());
632 t->setFont (f);
633 }
634
635
636