#include "GameWorldView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Building.h" #include "BuildingSystem.h" #include "BeltSystem.h" #include "Scrap.h" #include "ScrapSystem.h" #include "Ship.h" #include "ShipSystem.h" #include "Simulation.h" #include "SurfaceMask.h" #include "Tick.h" namespace { Rotation rotateClockwise(Rotation r) { switch (r) { case Rotation::North: return Rotation::East; case Rotation::East: return Rotation::South; case Rotation::South: return Rotation::West; case Rotation::West: return Rotation::North; } return Rotation::East; } Rotation rotateCounterClockwise(Rotation r) { switch (r) { case Rotation::North: return Rotation::West; case Rotation::East: return Rotation::North; case Rotation::South: return Rotation::East; case Rotation::West: return Rotation::South; } return Rotation::East; } ShipRole shipRole(const Ship& ship) { if (ship.isEnemy) { return ShipRole::Enemy; } if (ship.cargo.has_value()) { return ShipRole::Salvage; } if (ship.repairTool.has_value()) { return ShipRole::Repair; } return ShipRole::PlayerCombat; } QString toDisplayName(const std::string& id) { QString result; bool nextUpper = true; for (char c : id) { if (c == '_') { result += ' '; nextUpper = true; } else if (nextUpper) { result += static_cast(std::toupper(static_cast(c))); nextUpper = false; } else { result += c; } } return result; } QPoint portBodyTile(QPoint portTile, Rotation direction) { switch (direction) { case Rotation::East: return portTile + QPoint(-1, 0); case Rotation::West: return portTile + QPoint( 1, 0); case Rotation::North: return portTile + QPoint( 0, 1); case Rotation::South: return portTile + QPoint( 0, -1); } return portTile; } } // namespace GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config, const VisualsConfig* visuals, QWidget* parent) : QOpenGLWidget(parent) , m_sim(sim) , m_config(config) , m_visuals(visuals) , m_wallMs(0) , m_gameSpeedMultiplier(1.0) , m_prevNonZeroSpeed(1.0) , m_scrollXTiles(0.0f) , m_ghostRotation(Rotation::East) , m_ghostValid(false) , m_dragging(false) , m_demolishMode(false) , m_demolishHoverId(kInvalidEntityId) , m_debugDraw(false) , m_boxSelecting(false) , m_scrollLeft(false) , m_scrollRight(false) , m_gameOverShown(false) { setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); m_renderTimer = new QTimer(this); m_renderTimer->setInterval(16); connect(m_renderTimer, &QTimer::timeout, this, &GameWorldView::onFrame); m_renderTimer->start(); m_frameTimer.start(); } void GameWorldView::initializeGL() { // QPainter handles all rendering; no custom GL state needed. } void GameWorldView::onFrame() { const qint64 elapsed = m_frameTimer.restart(); m_wallMs += elapsed; // Advance simulation { const int ticks = m_tickDriver.advance( static_cast(elapsed), m_gameSpeedMultiplier); for (int i = 0; i < ticks; ++i) { m_sim->tick(); } } // Drain fire events → active beams { const std::vector fires = m_sim->drainFireEvents(); for (const FireEvent& fe : fires) { ActiveBeam beam; beam.event = fe; beam.emittedWallMs = m_wallMs; m_activeBeams.push_back(beam); } } // Drain schematic drop events → toasts { const std::vector drops = m_sim->drainSchematicDropEvents(); for (const SchematicDropEvent& ev : drops) { const QString shipName = toDisplayName(ev.schematicId); ToastEntry toast; if (ev.wasNewUnlock) { toast.text = "Schematic unlocked: " + shipName; } else { toast.text = shipName + " production level -> " + QString::number(ev.newLevel); } toast.createdWallMs = m_wallMs; m_toasts.push_back(toast); } } // Expire old beams { std::vector live; for (const ActiveBeam& b : m_activeBeams) { if (m_wallMs - b.emittedWallMs < kBeamLifetimeMs) { live.push_back(b); } } m_activeBeams = std::move(live); } // Expire old toasts { std::vector live; for (const ToastEntry& t : m_toasts) { if (m_wallMs - t.createdWallMs < kToastLifetimeMs) { live.push_back(t); } } m_toasts = std::move(live); } // Apply held scroll { const float delta = kScrollSpeedTilesPerSec * static_cast(elapsed) / 1000.0f; if (m_scrollLeft) { m_scrollXTiles -= delta; } if (m_scrollRight) { m_scrollXTiles += delta; } clampScroll(); } // Emit state update for header bar / build grid emit stateUpdated(m_sim->currentTick(), m_sim->buildingBlocksStock(), m_gameSpeedMultiplier); // Game over check if (m_sim->isGameOver() && !m_gameOverShown) { m_gameOverShown = true; m_gameSpeedMultiplier = 0.0; emit gameOver(); } update(); } void GameWorldView::paintGL() { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, false); drawTiles(painter); drawBuildings(painter); drawBeltItems(painter); drawScrap(painter); if (m_debugDraw) { drawDebugSensorRanges(painter); } drawShips(painter); drawBeams(painter); drawOverlays(painter); drawScreenSpace(painter); } // --------------------------------------------------------------------------- // Coordinate helpers // --------------------------------------------------------------------------- float GameWorldView::tilePx() const { if (m_config->world.heightTiles <= 0) { return 1.0f; } return static_cast(height()) / static_cast(m_config->world.heightTiles); } float GameWorldView::viewportWidthTiles() const { return static_cast(width()) / tilePx(); } QPointF GameWorldView::worldToWidget(QVector2D worldPos) const { return QPointF( static_cast((worldPos.x() - m_scrollXTiles) * tilePx()), static_cast(worldPos.y() * tilePx())); } QPointF GameWorldView::tileToWidget(QPoint tile) const { return worldToWidget(QVector2D(static_cast(tile.x()), static_cast(tile.y()))); } QPoint GameWorldView::widgetToTile(QPoint widgetPt) const { const float wx = static_cast(widgetPt.x()) / tilePx() + m_scrollXTiles; const float wy = static_cast(widgetPt.y()) / tilePx(); return QPoint(static_cast(std::floor(wx)), static_cast(std::floor(wy))); } QRectF GameWorldView::tileRect(QPoint tile) const { const QPointF tl = tileToWidget(tile); return QRectF(tl.x(), tl.y(), static_cast(tilePx()), static_cast(tilePx())); } QRect GameWorldView::viewportRect() const { const int left = static_cast(std::floor(m_scrollXTiles)) - 1; const int top = 0; const int right = static_cast(std::ceil(m_scrollXTiles + viewportWidthTiles())) + 1; const int bottom = m_config->world.heightTiles; return QRect(left, top, right - left, bottom - top); } float GameWorldView::asteroidLeftEdge() const { float leftX = -static_cast(m_config->world.regions.asteroidWidth); for (const Building& b : m_sim->buildings().allBuildings()) { for (const QPoint& cell : b.bodyCells) { if (static_cast(cell.x()) < leftX) { leftX = static_cast(cell.x()); } } } return leftX; } float GameWorldView::enemyStationRightEdge() const { float rightX = static_cast(m_config->world.regions.playerBufferWidth + m_config->world.regions.contestZoneWidth); for (const Building& b : m_sim->buildings().allBuildings()) { if (b.type == BuildingType::EnemyDefenceStation) { for (const QPoint& cell : b.bodyCells) { const float cx = static_cast(cell.x() + 1); if (cx > rightX) { rightX = cx; } } } } return rightX; } void GameWorldView::clampScroll() { const float leftBound = asteroidLeftEdge(); const float rightBound = enemyStationRightEdge() - viewportWidthTiles(); m_scrollXTiles = std::max(leftBound, std::min(m_scrollXTiles, rightBound)); } // --------------------------------------------------------------------------- // Placement helpers // --------------------------------------------------------------------------- const BuildingDef* GameWorldView::findBuildingDef(BuildingType type) const { for (const BuildingDef& def : m_config->buildings.buildings) { if (def.type == type) { return &def; } } return nullptr; } bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const { const BuildingDef* def = findBuildingDef(type); if (!def) { return false; } const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rot); for (const QPoint& relCell : parsed.bodyCells) { const QPoint worldCell = anchor + relCell; // Terrain check: S cells must be space (x >= 0), A cells must be asteroid (x < 0) bool isShipDock = false; for (const QPoint& dock : parsed.shipDockCells) { if (dock == relCell) { isShipDock = true; break; } } if (isShipDock && worldCell.x() < 0) { return false; } if (!isShipDock && worldCell.x() >= 0) { return false; } // Occupancy check if (m_sim->buildings().isTileOccupied(worldCell)) { return false; } } return true; } EntityId GameWorldView::buildingAtTile(QPoint tile) const { for (const Building& b : m_sim->buildings().allBuildings()) { for (const QPoint& cell : b.bodyCells) { if (cell == tile) { return b.id; } } } return kInvalidEntityId; } EntityId GameWorldView::siteAtTile(QPoint tile) const { for (const ConstructionSite& s : m_sim->buildings().allSites()) { for (const QPoint& cell : s.bodyCells) { if (cell == tile) { return s.id; } } } return kInvalidEntityId; } std::optional GameWorldView::entityPosition(EntityId id) const { const Ship* ship = m_sim->ships().findShip(id); if (ship) { return ship->position; } const Building* bldg = m_sim->buildings().findBuilding(id); if (bldg) { return QVector2D( bldg->anchor.x() + bldg->footprint.width() * 0.5f, bldg->anchor.y() + bldg->footprint.height() * 0.5f); } return std::nullopt; } void GameWorldView::stepSpeed(int delta) { const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 4.0 }; const int kCount = 5; int current = 2; for (int i = 0; i < kCount; ++i) { if (std::abs(kSpeeds[i] - m_gameSpeedMultiplier) < 0.001) { current = i; break; } } const int next = std::max(0, std::min(kCount - 1, current + delta)); setGameSpeed(kSpeeds[next]); } void GameWorldView::placeBlueprintAtTile(QPoint center) { const Blueprint& bp = *m_blueprintMode; for (const BlueprintBuilding& bb : bp.buildings) { if (!isValidPlacement(bb.type, center + bb.offset, bb.rotation)) { return; } } int totalCost = 0; for (const BlueprintBuilding& bb : bp.buildings) { const BuildingDef* def = findBuildingDef(bb.type); if (def) { totalCost += def->cost; } } if (m_sim->buildingBlocksStock() < totalCost) { return; } for (const BlueprintBuilding& bb : bp.buildings) { const EntityId id = m_sim->tryPlaceBuilding(bb.type, center + bb.offset, bb.rotation); if (id == kInvalidEntityId || bb.recipeId.empty()) { continue; } if (bb.type == BuildingType::Shipyard) { if (m_sim->isSchematicUnlocked(bb.recipeId)) { m_sim->buildings().setRecipe(id, bb.recipeId); } } else { m_sim->buildings().setRecipe(id, bb.recipeId); } } } void GameWorldView::placeAtTile(QPoint tile) { if (!m_builderType.has_value()) { return; } const BuildingType type = *m_builderType; if (!isValidPlacement(type, tile, m_ghostRotation)) { return; } if (type == BuildingType::Belt) { if (m_beltDragTiles.count(tile) > 0) { return; } if (!m_sim->buildings().isTileOccupied(tile)) { const EntityId id = m_sim->tryPlaceBuilding( type, tile, m_ghostRotation); if (id != kInvalidEntityId) { m_beltDragTiles.insert(tile); } } } else if (type == BuildingType::Splitter || type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit) { if (!m_sim->buildings().isTileOccupied(tile)) { m_sim->tryPlaceBuilding(type, tile, m_ghostRotation); } } else { m_sim->tryPlaceBuilding(type, tile, m_ghostRotation); } } // --------------------------------------------------------------------------- // Port glyph helper // --------------------------------------------------------------------------- void GameWorldView::drawPortGlyph(QPainter& painter, QPoint bodyTile, Rotation direction, const QColor& color) { const float px = tilePx(); const QRectF tr = tileRect(bodyTile); const QPointF center(tr.x() + static_cast(px) * 0.5, tr.y() + static_cast(px) * 0.5); QPointF offset; const char* ch; switch (direction) { case Rotation::East: offset = QPointF(px * 0.25f, 0); ch = ">"; break; case Rotation::West: offset = QPointF(-px * 0.25f, 0); ch = "<"; break; case Rotation::North: offset = QPointF(0, -px * 0.25f); ch = "^"; break; case Rotation::South: offset = QPointF(0, px * 0.25f); ch = "v"; break; default: return; } const qreal half = static_cast(px) * 0.3; const QPointF pos = center + offset; const QRectF textRect(pos.x() - half, pos.y() - half, half * 2.0, half * 2.0); QFont f = painter.font(); f.setPixelSize(std::max(6, static_cast(px * 0.4f))); painter.setFont(f); painter.setPen(color); painter.drawText(textRect, Qt::AlignCenter, QString::fromLatin1(ch)); } // --------------------------------------------------------------------------- // Rendering // --------------------------------------------------------------------------- void GameWorldView::drawTiles(QPainter& painter) { const int leftTile = static_cast(std::floor(m_scrollXTiles)) - 1; const int rightTile = leftTile + static_cast(std::ceil(viewportWidthTiles())) + 2; const int bottomTile = m_config->world.heightTiles; painter.setPen(Qt::NoPen); for (int x = leftTile; x <= rightTile; ++x) { const QColor& fill = (x < 0) ? m_visuals->asteroid.fill : m_visuals->space.fill; for (int y = 0; y < bottomTile; ++y) { painter.fillRect(tileRect(QPoint(x, y)), fill); } } } void GameWorldView::drawBuildings(QPainter& painter) { for (const Building& b : m_sim->buildings().allBuildings()) { const std::map::const_iterator it = m_visuals->buildings.find(b.type); if (it == m_visuals->buildings.end()) { continue; } const BuildingVisuals& bv = it->second; painter.setPen(Qt::NoPen); for (const QPoint& cell : b.bodyCells) { painter.fillRect(tileRect(cell), bv.fill); } const QPointF tl = tileToWidget(b.anchor); const QRectF bboxRect(tl.x(), tl.y(), b.footprint.width() * static_cast(tilePx()), b.footprint.height() * static_cast(tilePx())); painter.setPen(QPen(bv.outline, 1)); painter.setBrush(Qt::NoBrush); painter.drawRect(bboxRect); if (!bv.glyph.isEmpty()) { painter.setPen(bv.outline); painter.drawText(bboxRect, Qt::AlignCenter, bv.glyph); } for (const Port& port : b.outputPorts) { drawPortGlyph(painter, portBodyTile(port.tile, port.direction), port.direction, bv.outline); } bool selected = false; for (EntityId selId : m_selectedIds) { if (selId == b.id) { selected = true; break; } } if (selected) { painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2)); painter.setBrush(Qt::NoBrush); painter.drawRect(bboxRect.adjusted(-1, -1, 1, 1)); } } painter.setOpacity(0.5); for (const ConstructionSite& s : m_sim->buildings().allSites()) { const std::map::const_iterator it = m_visuals->buildings.find(s.type); if (it == m_visuals->buildings.end()) { continue; } const BuildingVisuals& bv = it->second; for (const QPoint& cell : s.bodyCells) { painter.fillRect(tileRect(cell), bv.fill); } const QPointF tl = tileToWidget(s.anchor); const QRectF bboxRect(tl.x(), tl.y(), s.footprint.width() * static_cast(tilePx()), s.footprint.height() * static_cast(tilePx())); painter.setPen(QPen(bv.outline, 1, Qt::DashLine)); painter.setBrush(Qt::NoBrush); painter.drawRect(bboxRect); const BuildingDef* siteDef = findBuildingDef(s.type); if (siteDef) { // Glyph + progress percentage const Tick durationTicks = secondsToTicks(siteDef->constructionTimeSeconds); int pct = 0; if (s.completesAt > 0 && durationTicks > 0) { const Tick elapsed = m_sim->currentTick() - (s.completesAt - durationTicks); pct = static_cast( std::max(Tick(0), std::min(durationTicks, elapsed)) * 100 / durationTicks); } const QString pctText = QString::number(pct) + "%"; painter.setPen(bv.outline); if (!bv.glyph.isEmpty()) { const QRectF topHalf(bboxRect.x(), bboxRect.y(), bboxRect.width(), bboxRect.height() * 0.5); const QRectF botHalf(bboxRect.x(), bboxRect.y() + bboxRect.height() * 0.5, bboxRect.width(), bboxRect.height() * 0.5); painter.drawText(topHalf, Qt::AlignCenter, bv.glyph); painter.drawText(botHalf, Qt::AlignCenter, pctText); } else { painter.drawText(bboxRect, Qt::AlignCenter, pctText); } // Port glyphs const ParsedSurfaceMask siteMask = parseSurfaceMask(siteDef->surfaceMask, s.rotation); for (const Port& port : siteMask.outputPorts) { const QPoint absBody = s.anchor + portBodyTile(port.tile, port.direction); drawPortGlyph(painter, absBody, port.direction, bv.outline); } } } painter.setOpacity(1.0); } void GameWorldView::drawBeltItems(QPainter& painter) { const float halfPx = tilePx() * 0.5f * 0.5f; const QRect vr = viewportRect(); m_sim->belts().forEachVisualItem(vr, [&](const VisualItem& vi) { const std::map::const_iterator it = m_visuals->items.find(vi.type.id); if (it == m_visuals->items.end()) { return; } const QPointF center = worldToWidget( QVector2D(static_cast(vi.worldPos.x()), static_cast(vi.worldPos.y()))); const QRectF rect(center.x() - halfPx, center.y() - halfPx, halfPx * 2, halfPx * 2); painter.fillRect(rect, it->second.fill); painter.setPen(QPen(it->second.outline, 1)); painter.setBrush(Qt::NoBrush); painter.drawRect(rect); }); } void GameWorldView::drawScrap(QPainter& painter) { const float r = tilePx() * 0.2f; for (const Scrap& scrap : m_sim->scraps().allScraps()) { const QPointF center = worldToWidget(scrap.position); painter.setBrush(QColor(128, 110, 90)); painter.setPen(QPen(QColor(50, 40, 30), 1)); painter.drawEllipse(center, static_cast(r), static_cast(r)); } } void GameWorldView::drawShips(QPainter& painter) { for (const Ship& ship : m_sim->ships().allShips()) { const ShipRole role = shipRole(ship); const std::map::const_iterator it = m_visuals->ships.find(role); if (it == m_visuals->ships.end()) { continue; } const QPointF center = worldToWidget(ship.position); const QVector2D vel = ship.velocity; const QVector2D dir = (vel.length() > 0.0001f) ? vel.normalized() : QVector2D(1.0f, 0.0f); const QVector2D perp(-dir.y(), dir.x()); const float fwd = tilePx() * 0.45f; const float side = tilePx() * 0.25f; QPolygonF tri; tri << QPointF(center.x() + static_cast(dir.x() * fwd), center.y() + static_cast(dir.y() * fwd)) << QPointF(center.x() + static_cast(perp.x() * side - dir.x() * side), center.y() + static_cast(perp.y() * side - dir.y() * side)) << QPointF(center.x() + static_cast(-perp.x() * side - dir.x() * side), center.y() + static_cast(-perp.y() * side - dir.y() * side)); painter.setPen(QPen(it->second.outline, 1)); painter.setBrush(it->second.fill); painter.drawPolygon(tri); } } void GameWorldView::drawDebugSensorRanges(QPainter& painter) { painter.setBrush(Qt::NoBrush); for (const Ship& ship : m_sim->ships().allShips()) { const ShipRole role = shipRole(ship); const std::map::const_iterator it = m_visuals->ships.find(role); if (it == m_visuals->ships.end()) { continue; } float range = 0.0f; if (ship.weapon.has_value()) { range = ship.weapon->range; } else if (ship.repairTool.has_value()) { range = ship.repairTool->range; } else if (ship.cargo.has_value()) { range = ship.cargo->collectionRange * 5.0f; } else { continue; } const QPointF center = worldToWidget(ship.position); const qreal radiusPx = static_cast(range) * static_cast(tilePx()); painter.setPen(QPen(it->second.outline, 1)); painter.drawEllipse(center, radiusPx, radiusPx); } } void GameWorldView::drawBeams(QPainter& painter) { painter.setPen(QPen(m_visuals->beams.color, m_visuals->beams.widthPx)); for (const ActiveBeam& beam : m_activeBeams) { const std::optional shooterPos = entityPosition(beam.event.shooter); const std::optional targetPos = entityPosition(beam.event.target); if (!shooterPos.has_value() || !targetPos.has_value()) { continue; } painter.drawLine(worldToWidget(*shooterPos), worldToWidget(*targetPos)); } } void GameWorldView::drawOverlays(QPainter& painter) { // Builder-mode ghost if (m_builderType.has_value()) { const BuildingDef* def = findBuildingDef(*m_builderType); if (def) { const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, m_ghostRotation); const QColor& ghostColor = m_ghostValid ? m_visuals->overlays.ghostValid : m_visuals->overlays.ghostInvalid; for (const QPoint& cell : parsed.bodyCells) { painter.fillRect(tileRect(m_ghostTile + cell), ghostColor); } for (const Port& port : parsed.outputPorts) { const QPoint absBody = m_ghostTile + portBodyTile(port.tile, port.direction); drawPortGlyph(painter, absBody, port.direction, Qt::white); } } } // Blueprint placement ghost if (m_blueprintMode.has_value()) { for (const BlueprintBuilding& bb : m_blueprintMode->buildings) { const QPoint anchor = m_blueprintGhostTile + bb.offset; const bool valid = isValidPlacement(bb.type, anchor, bb.rotation); const QColor& ghostColor = valid ? m_visuals->overlays.ghostValid : m_visuals->overlays.ghostInvalid; const BuildingDef* def = findBuildingDef(bb.type); if (!def) { continue; } const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, bb.rotation); for (const QPoint& cell : parsed.bodyCells) { painter.fillRect(tileRect(anchor + cell), ghostColor); } for (const Port& port : parsed.outputPorts) { drawPortGlyph(painter, anchor + portBodyTile(port.tile, port.direction), port.direction, Qt::white); } } } // Demolish hover tint if (m_demolishMode && m_demolishHoverId != kInvalidEntityId) { const Building* b = m_sim->buildings().findBuilding(m_demolishHoverId); if (b) { for (const QPoint& cell : b->bodyCells) { painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint); } } } // Box-select rectangle if (m_boxSelecting) { const QPoint tl(std::min(m_boxStartTile.x(), m_boxCurrentTile.x()), std::min(m_boxStartTile.y(), m_boxCurrentTile.y())); const QPoint br(std::max(m_boxStartTile.x(), m_boxCurrentTile.x()) + 1, std::max(m_boxStartTile.y(), m_boxCurrentTile.y()) + 1); const QRectF selRect(tileToWidget(tl), tileToWidget(br)); painter.setPen(QPen(m_visuals->overlays.selectionRect, 1)); painter.setBrush(Qt::NoBrush); painter.drawRect(selRect); } } void GameWorldView::drawScreenSpace(QPainter& painter) { painter.resetTransform(); const int margin = 8; const int toastW = 320; const int toastH = 36; const int spacing = 4; QFont toastFont = painter.font(); toastFont.setPointSize(m_visuals->toast.fontSize); painter.setFont(toastFont); int y = margin; for (const ToastEntry& toast : m_toasts) { const qint64 age = m_wallMs - toast.createdWallMs; double opacity = 1.0; if (age > kToastFadeStartMs) { opacity = 1.0 - static_cast(age - kToastFadeStartMs) / static_cast(kToastLifetimeMs - kToastFadeStartMs); opacity = std::max(0.0, opacity); } painter.setOpacity(opacity); const int x = width() - toastW - margin; const QRect toastRect(x, y, toastW, toastH); painter.fillRect(toastRect, m_visuals->toast.bg); painter.setPen(m_visuals->toast.fg); painter.drawText(toastRect.adjusted(8, 0, -8, 0), Qt::AlignVCenter | Qt::AlignLeft, toast.text); painter.setOpacity(1.0); y += toastH + spacing; } } // --------------------------------------------------------------------------- // Input // --------------------------------------------------------------------------- void GameWorldView::keyPressEvent(QKeyEvent* event) { if (event->isAutoRepeat()) { QOpenGLWidget::keyPressEvent(event); return; } switch (event->key()) { case Qt::Key_A: m_scrollLeft = true; break; case Qt::Key_D: m_scrollRight = true; break; case Qt::Key_Space: if (m_gameSpeedMultiplier > 0.0) { m_prevNonZeroSpeed = m_gameSpeedMultiplier; setGameSpeed(0.0); } else { setGameSpeed(m_prevNonZeroSpeed); } break; case Qt::Key_W: stepSpeed(+1); break; case Qt::Key_S: stepSpeed(-1); break; case Qt::Key_E: if (m_builderType.has_value()) { m_ghostRotation = rotateClockwise(m_ghostRotation); m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation); } else if (m_blueprintMode.has_value()) { for (BlueprintBuilding& bb : m_blueprintMode->buildings) { const BuildingDef* def = findBuildingDef(bb.type); if (!def) { continue; } const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation); int minX = INT_MAX, minY = INT_MAX; for (const QPoint& cell : mask.bodyCells) { const QPoint abs = bb.offset + cell; minX = std::min(minX, -abs.y()); minY = std::min(minY, abs.x()); } bb.offset = QPoint(minX, minY); bb.rotation = rotateClockwise(bb.rotation); } } break; case Qt::Key_Q: if (m_builderType.has_value()) { m_ghostRotation = rotateCounterClockwise(m_ghostRotation); m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation); } else if (m_blueprintMode.has_value()) { for (BlueprintBuilding& bb : m_blueprintMode->buildings) { const BuildingDef* def = findBuildingDef(bb.type); if (!def) { continue; } const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation); int minX = INT_MAX, minY = INT_MAX; for (const QPoint& cell : mask.bodyCells) { const QPoint abs = bb.offset + cell; minX = std::min(minX, abs.y()); minY = std::min(minY, -abs.x()); } bb.offset = QPoint(minX, minY); bb.rotation = rotateCounterClockwise(bb.rotation); } } break; case Qt::Key_Escape: emit escapeMenuRequested(); break; case Qt::Key_Backspace: toggleDemolishMode(); break; case Qt::Key_M: m_debugDraw = !m_debugDraw; break; default: QOpenGLWidget::keyPressEvent(event); break; } } void GameWorldView::keyReleaseEvent(QKeyEvent* event) { if (event->isAutoRepeat()) { QOpenGLWidget::keyReleaseEvent(event); return; } if (event->key() == Qt::Key_A) { m_scrollLeft = false; } if (event->key() == Qt::Key_D) { m_scrollRight = false; } QOpenGLWidget::keyReleaseEvent(event); } void GameWorldView::mousePressEvent(QMouseEvent* event) { if (event->button() != Qt::LeftButton) { if (event->button() == Qt::RightButton) { if (m_builderType.has_value()) { exitBuilderMode(); } else if (m_blueprintMode.has_value()) { exitBlueprintMode(); } } return; } const QPoint tile = widgetToTile(event->pos()); if (m_builderType.has_value()) { const BuildingType type = *m_builderType; if (type == BuildingType::Belt || type == BuildingType::Splitter) { m_dragging = true; m_beltDragTiles.clear(); placeAtTile(tile); } else { placeAtTile(tile); } } else if (m_blueprintMode.has_value()) { placeBlueprintAtTile(tile); } else if (m_demolishMode) { EntityId hovered = buildingAtTile(tile); if (hovered == kInvalidEntityId) { hovered = siteAtTile(tile); } if (hovered != kInvalidEntityId) { const Building* b = m_sim->buildings().findBuilding(hovered); const bool isProtected = b && (b->type == BuildingType::Hq || b->type == BuildingType::PlayerDefenceStation); if (!isProtected) { m_sim->demolish(hovered); m_demolishHoverId = kInvalidEntityId; } } } else { EntityId id = buildingAtTile(tile); if (id == kInvalidEntityId) { id = siteAtTile(tile); } if (id != kInvalidEntityId) { if (event->modifiers() & Qt::ControlModifier) { bool found = false; std::vector newSel; for (EntityId sel : m_selectedIds) { if (sel == id) { found = true; } else { newSel.push_back(sel); } } if (!found) { newSel.push_back(id); } m_selectedIds = newSel; } else { m_selectedIds = { id }; } emit selectionChanged(m_selectedIds); } else { if (!(event->modifiers() & Qt::ControlModifier)) { m_selectedIds.clear(); emit selectionChanged(m_selectedIds); } m_boxSelecting = true; m_boxStartTile = tile; m_boxCurrentTile = tile; } } } void GameWorldView::mouseMoveEvent(QMouseEvent* event) { const QPoint tile = widgetToTile(event->pos()); if (m_builderType.has_value()) { m_ghostTile = tile; m_ghostValid = isValidPlacement(*m_builderType, tile, m_ghostRotation); if (m_dragging) { placeAtTile(tile); } } else if (m_blueprintMode.has_value()) { m_blueprintGhostTile = tile; } else if (m_demolishMode) { m_demolishHoverId = buildingAtTile(tile); } else if (m_boxSelecting) { m_boxCurrentTile = tile; } } void GameWorldView::mouseReleaseEvent(QMouseEvent* event) { if (event->button() != Qt::LeftButton) { return; } if (m_dragging) { m_dragging = false; m_beltDragTiles.clear(); } if (m_boxSelecting) { m_boxSelecting = false; const int x0 = std::min(m_boxStartTile.x(), m_boxCurrentTile.x()); const int y0 = std::min(m_boxStartTile.y(), m_boxCurrentTile.y()); const int x1 = std::max(m_boxStartTile.x(), m_boxCurrentTile.x()); const int y1 = std::max(m_boxStartTile.y(), m_boxCurrentTile.y()); std::vector boxSel; for (const Building& b : m_sim->buildings().allBuildings()) { for (const QPoint& cell : b.bodyCells) { if (cell.x() >= x0 && cell.x() <= x1 && cell.y() >= y0 && cell.y() <= y1) { boxSel.push_back(b.id); break; } } } for (const ConstructionSite& s : m_sim->buildings().allSites()) { for (const QPoint& cell : s.bodyCells) { if (cell.x() >= x0 && cell.x() <= x1 && cell.y() >= y0 && cell.y() <= y1) { boxSel.push_back(s.id); break; } } } if (!(event->modifiers() & Qt::ControlModifier)) { m_selectedIds = boxSel; } else { for (EntityId id : boxSel) { bool found = false; for (EntityId sel : m_selectedIds) { if (sel == id) { found = true; break; } } if (!found) { m_selectedIds.push_back(id); } } } emit selectionChanged(m_selectedIds); } } // --------------------------------------------------------------------------- // Slots // --------------------------------------------------------------------------- void GameWorldView::toggleDemolishMode() { if (m_demolishMode) { m_demolishMode = false; m_demolishHoverId = kInvalidEntityId; } else { if (m_builderType.has_value()) { exitBuilderMode(); } if (m_blueprintMode.has_value()) { exitBlueprintMode(); } m_demolishMode = true; } emit demolishModeChanged(m_demolishMode); } void GameWorldView::enterBuilderMode(BuildingType type) { m_builderType = type; m_ghostRotation = Rotation::East; m_ghostValid = false; m_demolishMode = false; m_blueprintMode.reset(); emit demolishModeChanged(false); } void GameWorldView::enterBlueprintMode(Blueprint blueprint) { if (m_builderType.has_value()) { exitBuilderMode(); } m_demolishMode = false; emit demolishModeChanged(false); m_blueprintGhostTile = m_ghostTile; m_blueprintMode = std::move(blueprint); } void GameWorldView::exitBlueprintMode() { m_blueprintMode.reset(); emit blueprintModeExited(); } void GameWorldView::exitBuilderMode() { m_builderType.reset(); m_beltDragTiles.clear(); m_dragging = false; emit builderModeExited(); } double GameWorldView::gameSpeed() const { return m_gameSpeedMultiplier; } void GameWorldView::setGameSpeed(double multiplier) { m_gameSpeedMultiplier = multiplier; emit stateUpdated(m_sim->currentTick(), m_sim->buildingBlocksStock(), m_gameSpeedMultiplier); } void GameWorldView::resetForNewGame() { exitBuilderMode(); exitBlueprintMode(); m_activeBeams.clear(); m_toasts.clear(); m_ghostRotation = Rotation::East; m_ghostValid = false; m_demolishMode = false; m_demolishHoverId = kInvalidEntityId; emit demolishModeChanged(false); m_selectedIds.clear(); m_boxSelecting = false; m_scrollXTiles = 0.0f; m_scrollLeft = false; m_scrollRight = false; m_gameOverShown = false; m_gameSpeedMultiplier = 1.0; m_prevNonZeroSpeed = 1.0; emit selectionChanged({}); update(); }