#include "ArenaView.h" #include #include #include #include #include #include #include "ArenaSimulation.h" #include "Building.h" #include "BuildingSystem.h" #include "EntityHitTest.h" #include "EntitySelectedEvent.h" #include "EventManager.h" #include "FacingComponent.h" #include "FactionComponent.h" #include "HealthComponent.h" #include "PositionComponent.h" #include "ScrapSystem.h" #include "ShipIdentityComponent.h" #include "StationBodyComponent.h" namespace { } // namespace ArenaView::ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals, QWidget* parent) : QOpenGLWidget(parent) , m_sim(sim) , m_visuals(visuals) , m_wallMs(0) , m_gameSpeedMultiplier(1.0) , m_prevNonZeroSpeed(1.0) , m_rng(std::random_device{}()) , m_finishedEmitted(false) { setFocusPolicy(Qt::StrongFocus); m_renderTimer = new QTimer(this); m_renderTimer->setInterval(16); connect(m_renderTimer, &QTimer::timeout, this, &ArenaView::onFrame); m_renderTimer->start(); m_frameTimer.start(); } void ArenaView::setGameSpeed(double multiplier) { if (multiplier > 0.001) { m_prevNonZeroSpeed = multiplier; } m_gameSpeedMultiplier = multiplier; emit speedChanged(multiplier); } double ArenaView::gameSpeed() const { return m_gameSpeedMultiplier; } void ArenaView::stopRendering() { m_renderTimer->stop(); } void ArenaView::togglePause() { if (m_gameSpeedMultiplier < 0.001) { setGameSpeed(m_prevNonZeroSpeed); } else { setGameSpeed(0.0); } } void ArenaView::onFrame() { const qint64 elapsed = m_frameTimer.restart(); m_wallMs += elapsed; { const int ticks = m_tickDriver.advance( static_cast(elapsed), m_gameSpeedMultiplier); for (int i = 0; i < ticks; ++i) { m_sim->tickOnce(); } } { const std::vector fires = m_sim->drainFireEvents(); for (const FireEvent& fe : fires) { float maxRadius = 0.125f; if (m_sim->admin().isValid(fe.target) && m_sim->admin().hasAll(fe.target)) { const StationBodyComponent& sb = m_sim->admin().get(fe.target); const int shorter = std::min(sb.footprint.width(), sb.footprint.height()); maxRadius = shorter / 2.0f; } std::uniform_real_distribution angleDist(0.0f, 6.28318530f); std::uniform_real_distribution radiusDist(0.0f, maxRadius); const float angle = angleDist(m_rng); const float radius = radiusDist(m_rng); ActiveBeam beam; beam.event = fe; beam.emittedWallMs = m_wallMs; beam.targetOffset = QVector2D(radius * std::cos(angle), radius * std::sin(angle)); m_activeBeams.push_back(beam); } } { std::vector live; for (const ActiveBeam& b : m_activeBeams) { if (m_wallMs - b.emittedWallMs < kBeamLifetimeMs) { live.push_back(b); } } m_activeBeams = std::move(live); } if (m_sim->isFinished() && !m_finishedEmitted) { m_finishedEmitted = true; emit finished(); } update(); } void ArenaView::paintGL() { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, false); drawTiles(painter); drawBuildings(painter); drawStations(painter); drawScrap(painter); drawShips(painter); drawBeams(painter); } // --------------------------------------------------------------------------- // Coordinate helpers // --------------------------------------------------------------------------- float ArenaView::tilePx() const { const ArenaConfig& ac = m_sim->arenaConfig(); const int totalWidth = ac.playerBufferWidth_tiles + ac.contestZoneWidth_tiles + ac.enemyBufferWidth_tiles; const int totalHeight = ac.heightTiles; if (totalWidth <= 0 || totalHeight <= 0) { return 1.0f; } const float pxPerTileH = static_cast(height()) / static_cast(totalHeight); const float pxPerTileW = static_cast(width()) / static_cast(totalWidth); return std::min(pxPerTileH, pxPerTileW); } QPointF ArenaView::worldToWidget(QVector2D worldPos) const { return QPointF( static_cast(worldPos.x() * tilePx()), static_cast(worldPos.y() * tilePx())); } QPointF ArenaView::tileToWidget(QPoint tile) const { return worldToWidget(QVector2D(static_cast(tile.x()), static_cast(tile.y()))); } QRectF ArenaView::tileRect(QPoint tile) const { const QPointF tl = tileToWidget(tile); return QRectF(tl.x(), tl.y(), static_cast(tilePx()), static_cast(tilePx())); } std::optional ArenaView::entityPosition(entt::entity entity) const { if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll(entity)) { return std::nullopt; } return m_sim->admin().get(entity).value; } QVector2D ArenaView::widgetToWorld(QPoint widgetPt) const { const float px = tilePx(); if (px < 0.001f) { return QVector2D(0.0f, 0.0f); } return QVector2D(static_cast(widgetPt.x()) / px, static_cast(widgetPt.y()) / px); } void ArenaView::mousePressEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton) { const QVector2D worldPos = widgetToWorld(event->pos()); entt::entity hit = entityAtWorldPos(m_sim->admin(), worldPos); if (hit != entt::null) { m_selectedEntity = hit; } else { m_selectedEntity = std::nullopt; } EventManager::getInstance()->sendEventImmediately( std::make_shared(m_selectedEntity)); } QOpenGLWidget::mousePressEvent(event); } // --------------------------------------------------------------------------- // Rendering // --------------------------------------------------------------------------- void ArenaView::drawTiles(QPainter& painter) { const ArenaConfig& ac = m_sim->arenaConfig(); const int totalWidth = ac.playerBufferWidth_tiles + ac.contestZoneWidth_tiles + ac.enemyBufferWidth_tiles; const int totalHeight = ac.heightTiles; painter.setPen(Qt::NoPen); for (int x = 0; x < totalWidth; ++x) { for (int y = 0; y < totalHeight; ++y) { painter.fillRect(tileRect(QPoint(x, y)), m_visuals->space.fill); } } } void ArenaView::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); } } } void ArenaView::drawScrap(QPainter& painter) { const float r = tilePx() * 0.2f; for (const ScrapInfo& scrap : m_sim->scraps().allScrapInfo()) { 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 ArenaView::drawStations(QPainter& painter) { m_sim->admin().forEach( [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f, const HealthComponent& h) { const BuildingType visType = f.isEnemy ? BuildingType::EnemyDefenceStation : BuildingType::PlayerDefenceStation; const std::map::const_iterator it = m_visuals->buildings.find(visType); if (it == m_visuals->buildings.end()) { return; } const BuildingVisuals& bv = it->second; painter.setPen(Qt::NoPen); for (const QPoint& cell : sb.bodyCells) { painter.fillRect(tileRect(cell), bv.fill); } const QPointF tl = tileToWidget(sb.anchor); const QRectF bboxRect(tl.x(), tl.y(), sb.footprint.width() * static_cast(tilePx()), sb.footprint.height() * static_cast(tilePx())); painter.setPen(QPen(bv.outline, 1)); painter.setBrush(Qt::NoBrush); painter.drawRect(bboxRect); if (h.maxHp > 0.0f) { const float fraction = std::max(0.0f, h.hp / h.maxHp); const qreal barH = static_cast(tilePx()) * 0.12; const qreal barY = bboxRect.bottom() + 1.0; const qreal barW = bboxRect.width(); painter.fillRect(QRectF(bboxRect.left(), barY, barW, barH), QColor(60, 60, 60)); painter.fillRect(QRectF(bboxRect.left(), barY, barW * static_cast(fraction), barH), f.isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60)); } if (m_selectedEntity.has_value() && *m_selectedEntity == e) { painter.setPen(QPen(QColor(255, 255, 0), 2)); painter.setBrush(Qt::NoBrush); painter.drawRect(bboxRect.adjusted(-2, -2, 2, 2)); } }); } void ArenaView::drawShips(QPainter& painter) { m_sim->admin().forEach( [&](entt::entity e, const ShipIdentityComponent& si, const PositionComponent& pos, const FacingComponent& facing, const FactionComponent& fac, const HealthComponent& h) { const std::map::const_iterator it = m_visuals->ships.find(si.schematicId); if (it == m_visuals->ships.end()) { return; } const QPointF center = worldToWidget(pos.value); const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians)); 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); if (h.maxHp > 0.0f) { const float fraction = std::max(0.0f, h.hp / h.maxHp); const qreal barW = static_cast(fwd) * 2.0; const qreal barH = static_cast(tilePx()) * 0.12; const qreal barX = center.x() - static_cast(fwd); const qreal barY = center.y() + static_cast(fwd) + 1.0; painter.fillRect(QRectF(barX, barY, barW, barH), QColor(60, 60, 60)); painter.fillRect(QRectF(barX, barY, barW * static_cast(fraction), barH), fac.isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60)); } if (m_selectedEntity.has_value() && *m_selectedEntity == e) { const qreal radius = static_cast(tilePx()) * 0.55; painter.setPen(QPen(QColor(255, 255, 0), 2)); painter.setBrush(Qt::NoBrush); painter.drawEllipse(center, radius, radius); } }); } void ArenaView::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 + beam.targetOffset)); } }