File indexing completed on 2024-04-21 04:02:29

0001 /*
0002     This file is part of Killbots.
0003 
0004     SPDX-FileCopyrightText: 2007-2009 Parker Coates <coates@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "scene.h"
0010 
0011 #include "numericdisplayitem.h"
0012 #include "renderer.h"
0013 #include "settings.h"
0014 
0015 #include <KGamePopupItem>
0016 
0017 #include "killbots_debug.h"
0018 
0019 #include <QPainter>
0020 #include <QGraphicsSceneMouseEvent>
0021 #include <QGraphicsView>
0022 
0023 #include <cmath>
0024 
0025 Killbots::Scene::Scene(QObject *parent)
0026     : QGraphicsScene(parent),
0027       m_hero(nullptr),
0028       m_rows(0),
0029       m_columns(0)
0030 {
0031     setItemIndexMethod(QGraphicsScene::NoIndex);
0032 }
0033 
0034 Killbots::Scene::~Scene()
0035 {
0036 }
0037 
0038 void Killbots::Scene::addNumericDisplay(NumericDisplayItem *displayItem)
0039 {
0040     addItem(displayItem);
0041     displayItem->setPos(-1000000, 0);
0042     m_numericDisplays << displayItem;
0043 }
0044 
0045 void Killbots::Scene::setGridSize(int rows, int columns)
0046 {
0047     if (m_rows != rows || m_columns != columns) {
0048         m_rows = rows;
0049         m_columns = columns;
0050     }
0051 }
0052 
0053 void Killbots::Scene::forgetHero()
0054 {
0055     m_hero = nullptr;
0056 }
0057 
0058 Killbots::Sprite *Killbots::Scene::createSprite(SpriteType type, QPoint position)
0059 {
0060     Sprite *sprite = new Sprite();
0061     sprite->setSpriteType(type);
0062     sprite->setRenderSize(m_cellSize);
0063     sprite->enqueueGridPos(position);
0064     updateSpritePos(sprite, position);
0065     sprite->setTransform(QTransform::fromScale(0, 0), true);
0066     // A bit of a hack, but we use the sprite type for stacking order.
0067     sprite->setZValue(type);
0068 
0069     addItem(sprite);
0070 
0071     if (type == Hero) {
0072         m_hero = sprite;
0073     }
0074 
0075     return sprite;
0076 }
0077 
0078 void Killbots::Scene::animateSprites(const QList<Sprite *> &newSprites,
0079                                      const QList<Sprite *> &slidingSprites,
0080                                      const QList<Sprite *> &teleportingSprites,
0081                                      const QList<Sprite *> &destroyedSprites,
0082                                      qreal value
0083                                     ) const
0084 {
0085     static bool halfDone = false;
0086 
0087     if (value == 0.0) {
0088         halfDone = false;
0089     } else if (value < 1.0) {
0090         for (Sprite *sprite : newSprites) {
0091             sprite->resetTransform();
0092             sprite->setTransform(QTransform::fromScale(value, value), true);
0093         }
0094 
0095         for (Sprite *sprite : slidingSprites) {
0096             QPointF posInGridCoordinates = value * QPointF(sprite->nextGridPos() - sprite->currentGridPos()) + sprite->currentGridPos();
0097             sprite->setPos(QPointF(posInGridCoordinates.x() * m_cellSize.width(), posInGridCoordinates.y() * m_cellSize.height()));
0098         }
0099 
0100         qreal scaleFactor = value < 0.5
0101                             ? 1.0 - 2 * value
0102                             : 2 * value - 1.0;
0103 
0104         if (!halfDone && value >= 0.5) {
0105             halfDone = true;
0106             for (Sprite *sprite : teleportingSprites) {
0107                 updateSpritePos(sprite, sprite->nextGridPos());
0108             }
0109         }
0110 
0111         for (Sprite *sprite : teleportingSprites) {
0112             sprite->resetTransform();
0113             sprite->setTransform(QTransform::fromScale(scaleFactor, scaleFactor), true);
0114         }
0115 
0116         for (Sprite *sprite : destroyedSprites) {
0117             sprite->resetTransform();
0118             sprite->setTransform(QTransform::fromScale(1 - value, 1 - value), true);
0119             sprite->setTransform(QTransform().rotate(value * 180), true);
0120         }
0121     } else {
0122         for (auto& sprites : {newSprites, slidingSprites, teleportingSprites}) {
0123             for (Sprite *sprite : sprites) {
0124                 sprite->resetTransform();
0125                 sprite->advanceGridPosQueue();
0126                 updateSpritePos(sprite, sprite->currentGridPos());
0127             }
0128         }
0129 
0130         qDeleteAll(destroyedSprites);
0131     }
0132 }
0133 
0134 void Killbots::Scene::doLayout()
0135 {
0136     QSize size = views().first()->size();
0137 
0138     // If no game has been started
0139     if (m_rows == 0 || m_columns == 0) {
0140         setSceneRect(QRectF(QPointF(0, 0), size));
0141         return;
0142     }
0143 
0144     //qCDebug(KILLBOTS_LOG) << "Laying out scene at" << size;
0145 
0146     // Make certain layout properties proportional to the scene height,
0147     // but clamp them between reasonable values. There's probably room for more
0148     // tweaking here.
0149     const int baseDimension = qMin(size.width(), size.height()) / 35;
0150     const int spacing = qBound(5, baseDimension, 15);
0151     const int newFontPixelSize = qBound(QFontInfo(QFont()).pixelSize(), baseDimension, 25);
0152     const qreal aspectRatio = Renderer::self()->aspectRatio();
0153 
0154     QSize displaySize;
0155     // If the font size has changed, resize all the displays (visible or not).
0156     if (m_numericDisplays.first()->font().pixelSize() != newFontPixelSize) {
0157         QFont font;
0158         font.setPixelSize(newFontPixelSize);
0159 
0160         for (NumericDisplayItem *display : std::as_const(m_numericDisplays)) {
0161             display->setFont(font);
0162             displaySize = displaySize.expandedTo(display->preferredSize());
0163         }
0164         for (NumericDisplayItem *display : std::as_const(m_numericDisplays)) {
0165             display->setRenderSize(displaySize);
0166         }
0167     } else {
0168         displaySize = m_numericDisplays.first()->boundingRect().size().toSize();
0169     }
0170 
0171     // The rest of the function deals only with a list of visible displays.
0172     QList<NumericDisplayItem *> visibleDisplays;
0173     for (NumericDisplayItem *display : std::as_const(m_numericDisplays)) {
0174         if (display->isVisible()) {
0175             visibleDisplays << display;
0176         }
0177     }
0178 
0179     // Determine the total width required to arrange the displays horizontally.
0180     const int widthOfDisplaysOnTop = visibleDisplays.size() * displaySize.width()
0181                                      + (visibleDisplays.size() - 1) * spacing;
0182 
0183     // The displays can either be placed centred, across the top of the
0184     // scene or top-aligned, down the side of the scene. We first calculate
0185     // what the cell size would be for both options.
0186     int availableWidth = size.width() - 3 * spacing - displaySize.width();
0187     int availableHeight = size.height() - 2 * spacing;
0188     const qreal cellWidthSide = (availableWidth / m_columns < availableHeight / m_rows * aspectRatio)
0189                                 ? availableWidth / m_columns
0190                                 : availableHeight / m_rows * aspectRatio;
0191 
0192     availableWidth = size.width() - 2 * spacing;
0193     availableHeight = size.height() - 3 * spacing - displaySize.height();
0194     const qreal cellWidthTop = (availableWidth / m_columns < availableHeight / m_rows * aspectRatio)
0195                                ? availableWidth / m_columns
0196                                : availableHeight / m_rows * aspectRatio;
0197 
0198     // If placing the displays on top would result in larger cells, we take
0199     // that option, but only if the displays would actually fit.
0200     const bool displaysOnTop = (cellWidthTop > cellWidthSide && size.width() > widthOfDisplaysOnTop);
0201     const qreal newCellWidth = displaysOnTop ? cellWidthTop : cellWidthSide;
0202     m_cellSize = QSize(qRound(newCellWidth), qRound(newCellWidth / aspectRatio));
0203 
0204     const auto items = this->items();
0205     for (QGraphicsItem *item : items) {
0206         Sprite *sprite = qgraphicsitem_cast<Sprite *>(item);
0207         if (sprite) {
0208             sprite->setRenderSize(m_cellSize);
0209             updateSpritePos(sprite, sprite->currentGridPos());
0210         }
0211     }
0212 
0213     if (displaysOnTop) {
0214         // Set the sceneRect to centre the grid if possible, but ensure the display items are visible
0215         const qreal sceneRectXPos = -(size.width() - m_cellSize.width() * (m_columns - 1)) / 2.0;
0216         const qreal centeredYPos = - (size.height() - m_cellSize.height() * (m_rows - 1)) / 2.0;
0217         const qreal indentedYPos = - (m_cellSize.height() / 2.0 + 2 * spacing + displaySize.height());
0218         const qreal sceneRectYPos = qMin(centeredYPos, indentedYPos);
0219 
0220         // Position the display items centered at the top of the scene
0221         const qreal displayYPos = (sceneRectYPos - (displaySize.height() + m_cellSize.height() / 2.0)) / 2;
0222 
0223         int xPos = sceneRectXPos + (size.width() - widthOfDisplaysOnTop) / 2.0;
0224         for (NumericDisplayItem *display : std::as_const(visibleDisplays)) {
0225             display->setPos(xPos, displayYPos);
0226             xPos += displaySize.width() + spacing;
0227         }
0228 
0229         setSceneRect(QRectF(sceneRectXPos, sceneRectYPos, size.width(), size.height()));
0230     } else {
0231         qreal sceneRectXPos;
0232         const qreal centeredXPos = - (size.width() - m_cellSize.width() * (m_columns - 1)) / 2.0;
0233         const qreal sceneRectYPos = -(size.height() - m_cellSize.height() * (m_rows - 1)) / 2.0;
0234         qreal displayXPos;
0235 
0236         // If the application layout is LTR, place the displays on left,
0237         // otherwise, place them on the right.
0238         if (views().first()->layoutDirection() == Qt::LeftToRight) {
0239             // Set the sceneRect to centre the grid if possible, but ensure the display items are visible
0240             const qreal indentedXPos = - (m_cellSize.width() / 2.0 + 2 * spacing + displaySize.width());
0241             sceneRectXPos = qMin(centeredXPos, indentedXPos);
0242 
0243             // Position the display items to the left of the grid
0244             displayXPos = - (spacing + displaySize.width() + m_cellSize.width() / 2);
0245         } else {
0246             // Set the sceneRect to centre the grid if possible, but ensure the display items are visible
0247             const qreal indentedXPos = (m_cellSize.width() * m_columns + 1 * spacing + displaySize.width()) - size.width();
0248             sceneRectXPos = qMax(centeredXPos, indentedXPos);
0249 
0250             // Position the display items to the right of the grid
0251             displayXPos = m_cellSize.width() * (m_columns - 0.5) + spacing;
0252         }
0253 
0254         int yPos = -m_cellSize.height() / 2;
0255         for (NumericDisplayItem *display : std::as_const(visibleDisplays)) {
0256             display->setPos(displayXPos, yPos);
0257             yPos += displaySize.height() + spacing;
0258         }
0259 
0260         setSceneRect(QRectF(sceneRectXPos, sceneRectYPos, size.width(), size.height()));
0261     }
0262 
0263     // Update the scene background
0264     QPainter p;
0265     QRect gridRect(-sceneRect().x() - m_cellSize.width() / 2,
0266                    -sceneRect().y() - m_cellSize.height() / 2,
0267                    m_columns * m_cellSize.width(),
0268                    m_rows * m_cellSize.height()
0269                   );
0270 
0271     QPixmap unrotated = Renderer::self()->spritePixmap(QStringLiteral("background"), size);
0272     p.begin(&unrotated);
0273     p.drawTiledPixmap(gridRect, Renderer::self()->spritePixmap(QStringLiteral("cell"), m_cellSize));
0274     p.end();
0275 
0276     // The background brush begins painting at 0,0 but our sceneRect doesn't
0277     // start at 0,0 so we have to "rotate" the pixmap so that it looks right
0278     // when tiled.
0279     QPixmap background(size);
0280     background.fill(Qt::transparent);
0281     p.begin(&background);
0282     p.drawTiledPixmap(background.rect(), unrotated, -sceneRect().topLeft().toPoint());
0283     p.end();
0284 
0285     setBackgroundBrush(background);
0286 
0287     update();
0288 }
0289 
0290 void Killbots::Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
0291 {
0292     getMouseDirection(event->scenePos());
0293     QGraphicsScene::mouseMoveEvent(event);
0294 }
0295 
0296 void Killbots::Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
0297 {
0298     HeroAction actionFromPosition = getMouseDirection(event->scenePos());
0299 
0300     if (actionFromPosition != NoAction) {
0301         Settings::ClickAction userAction = Settings::Nothing;
0302 
0303         if (event->button() == Qt::LeftButton) {
0304             if (event->modifiers() & Qt::ControlModifier) {
0305                 userAction = Settings::middleClickAction();
0306             } else {
0307                 userAction = Settings::Step;
0308             }
0309         } else if (event->button() == Qt::RightButton) {
0310             userAction = Settings::rightClickAction();
0311         } else if (event->button() == Qt::MiddleButton) {
0312             userAction = Settings::middleClickAction();
0313         }
0314 
0315         if (userAction == Settings::Step) {
0316             Q_EMIT clicked(actionFromPosition);
0317         } else if (userAction == Settings::RepeatedStep) {
0318             Q_EMIT clicked(-actionFromPosition - 1);
0319         } else if (userAction == Settings::Teleport) {
0320             Q_EMIT clicked(Teleport);
0321         } else if (userAction == Settings::TeleportSafely) {
0322             Q_EMIT clicked(TeleportSafely);
0323         } else if (userAction == Settings::TeleportSafelyIfPossible) {
0324             Q_EMIT clicked(TeleportSafelyIfPossible);
0325         } else if (userAction == Settings::WaitOutRound) {
0326             Q_EMIT clicked(WaitOutRound);
0327         }
0328     }
0329 
0330     QGraphicsScene::mouseReleaseEvent(event);
0331 }
0332 
0333 Killbots::HeroAction Killbots::Scene::getMouseDirection(QPointF cursorPosition) const
0334 {
0335     HeroAction result;
0336     const bool heroOnScreen = m_hero && sceneRect().contains(m_hero->sceneBoundingRect());
0337 
0338     if (heroOnScreen && !popupAtPosition(cursorPosition)) {
0339         if (m_hero->sceneBoundingRect().contains(cursorPosition)) {
0340             result = Hold;
0341         } else {
0342             const qreal piOver4 = 0.78539816339744830961566L;
0343 
0344             QPointF delta = cursorPosition - m_hero->sceneBoundingRect().center();
0345             int direction = qRound(atan2(-delta.y(), delta.x()) / piOver4);
0346             if (direction < 0) {
0347                 direction += 8;
0348             }
0349 
0350             result = static_cast<HeroAction>(direction);
0351         }
0352 
0353         views().first()->setCursor(Renderer::self()->cursorFromAction(result));
0354     } else {
0355         views().first()->unsetCursor();
0356         result = NoAction;
0357     }
0358 
0359     return result;
0360 }
0361 
0362 bool Killbots::Scene::popupAtPosition(QPointF position) const
0363 {
0364     const auto itemsAtPos = items(position);
0365     for (QGraphicsItem *item : itemsAtPos) {
0366         if (dynamic_cast<KGamePopupItem *>(item) != nullptr) {
0367             return true;
0368         }
0369     }
0370     return false;
0371 }
0372 
0373 void Killbots::Scene::updateSpritePos(Sprite *sprite, QPoint gridPosition) const
0374 {
0375     sprite->setPos(gridPosition.x() * m_cellSize.width(), gridPosition.y() * m_cellSize.height());
0376 }
0377 
0378 #include "moc_scene.cpp"