File indexing completed on 2025-02-16 03:48:18
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"