#include "ArenaView.h" #include "FactoryQueries.h" #include #include #include #include #include #include #include #include #include "ArenaSimulation.h" #include "AttackBehavior.h" #include "Building.h" #include "BuildingSystem.h" #include "EntityHitTest.h" #include "EntitySelectionChangedEvent.h" #include "EventManager.h" #include "FacingComponent.h" #include "FactionComponent.h" #include "GameSpeedChangedEvent.h" #include "HealthComponent.h" #include "PositionComponent.h" #include "RepairBehavior.h" #include "SalvageScrapBehavior.h" #include "DebrisSystem.h" #include "SensorRangeComponent.h" #include "ShipIdentityComponent.h" #include "StationBodyComponent.h" #include "DebrisComponent.h" #include "WorldPrimitives.h" namespace { } // namespace ArenaView::ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals, QWidget* parent) : QOpenGLWidget(parent) , m_sim(sim) , m_visuals(visuals) , m_gameSpeedMultiplier(1.0) , m_prevNonZeroSpeed(1.0) , m_rng(std::random_device{}()) , m_finishedEmitted(false) , m_debugDraw(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(); registerForEvent(); } ArenaView::~ArenaView() { unregisterForEvent(); } void ArenaView::setGameSpeed(double multiplier) { if (multiplier > 0.001) { m_prevNonZeroSpeed = multiplier; } m_gameSpeedMultiplier = multiplier; EventManager::getInstance()->sendEventImmediately( std::make_shared(multiplier)); } double ArenaView::getGameSpeed() 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(); { const int ticks = m_tickDriver.advance( static_cast(elapsed), m_gameSpeedMultiplier); for (int i = 0; i < ticks; ++i) { m_sim->tickOnce(); } } // Emit fire events via EventManager { const std::vector fires = m_sim->drainBeamFiredEvents(); for (const BeamFiredEvent& fe : fires) { EventManager::getInstance()->sendEventImmediately( std::make_shared(fe)); } } // Expire old beams. Lifetime is measured in game ticks so beams stay // visible while the simulation is paused or slowed (REQ-SHP-FIRING-BEAM). { const Tick now = m_sim->getCurrentTick(); std::vector live; for (const ActiveBeam& b : m_activeBeams) { if (now - b.event.emittedAt < kBeamLifetimeTicks) { live.push_back(b); } } m_activeBeams = std::move(live); } if (m_sim->isFinished() && !m_finishedEmitted) { m_finishedEmitted = true; } update(); } void ArenaView::handleEvent(std::shared_ptr event) { float maxRadius = 0.125f; if (m_sim->getAdmin().isValid(event->target) && m_sim->getAdmin().hasAll(event->target)) { const StationBodyComponent& sb = m_sim->getAdmin().get(event->target); const int shorter = std::min(sb.footprint.width(), sb.footprint.height()); maxRadius = shorter / 2.0f; } else if (m_sim->getAdmin().isValid(event->target) && m_sim->getAdmin().hasAll(event->target)) { maxRadius = 0.1f; } 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 = *event; beam.targetOffset = QVector2D(radius * std::cos(angle), radius * std::sin(angle)); m_activeBeams.push_back(beam); } void ArenaView::paintGL() { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, false); // One transform snapshot for the whole frame; every draw below reads the // viewport through it. const WorldCoordinates coordinates = getCoordinates(); drawTiles(painter, coordinates); drawBuildings(painter, coordinates); drawStations(painter, coordinates); drawDebris(painter, coordinates); if (m_debugDraw) { drawDebugSensorRanges(painter, coordinates); drawDebugTargetLines(painter, coordinates); } drawShips(painter, coordinates); drawBeams(painter, coordinates); } // --------------------------------------------------------------------------- // Coordinate helpers // --------------------------------------------------------------------------- WorldCoordinates ArenaView::getCoordinates() const { // The arena is a fixed, fully visible world — unlike the game view it has no // scrolling, so the tile size comes from fitting the whole arena in the widget. const ArenaConfig& ac = m_sim->getArenaConfig(); const int totalWidth = ac.playerBufferWidth_tiles + ac.contestZoneWidth_tiles + ac.enemyBufferWidth_tiles; return WorldCoordinates::fitToWorld(size(), totalWidth, ac.heightTiles); } std::optional ArenaView::entityPosition(entt::entity entity) const { if (!m_sim->getAdmin().isValid(entity) || !m_sim->getAdmin().hasAll(entity)) { return std::nullopt; } return m_sim->getAdmin().get(entity).value; } void ArenaView::mousePressEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton) { const QVector2D worldPos = getCoordinates().widgetToWorld(event->pos()); entt::entity hit = entityAtWorldPos(m_sim->getAdmin(), worldPos); if (hit != entt::null) { m_selectedEntity = hit; } else { m_selectedEntity = std::nullopt; } // The arena is strictly single-select; emit a vector of size 0 or 1. std::vector selection; if (m_selectedEntity.has_value()) { selection.push_back(*m_selectedEntity); } EventManager::getInstance()->sendEventImmediately( std::make_shared(selection)); } QOpenGLWidget::mousePressEvent(event); } void ArenaView::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_F3) { m_debugDraw = !m_debugDraw; return; } QOpenGLWidget::keyPressEvent(event); } // --------------------------------------------------------------------------- // Rendering // --------------------------------------------------------------------------- void ArenaView::drawTiles(QPainter& painter, const WorldCoordinates& coordinates) { const ArenaConfig& ac = m_sim->getArenaConfig(); 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(coordinates.tileRect(QPoint(x, y)), m_visuals->space.fill); } } } void ArenaView::drawBuildings(QPainter& painter, const WorldCoordinates& coordinates) { for (const Building& b : getAllBuildings(m_sim->getFactoryState())) { 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(coordinates.tileRect(cell), bv.fill); } const QPointF tl = coordinates.tileToWidget(b.anchor); const QRectF bboxRect(tl.x(), tl.y(), b.footprint.width() * static_cast(coordinates.getTilePx()), b.footprint.height() * static_cast(coordinates.getTilePx())); 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::drawDebris(QPainter& painter, const WorldCoordinates& coordinates) { for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin())) { drawDebrisMarker(painter, coordinates, coordinates.worldToWidget(debris.position)); } } void ArenaView::drawStations(QPainter& painter, const WorldCoordinates& coordinates) { m_sim->getAdmin().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(coordinates.tileRect(cell), bv.fill); } const QPointF tl = coordinates.tileToWidget(sb.anchor); const QRectF bboxRect(tl.x(), tl.y(), sb.footprint.width() * static_cast(coordinates.getTilePx()), sb.footprint.height() * static_cast(coordinates.getTilePx())); painter.setPen(QPen(bv.outline, 1)); painter.setBrush(Qt::NoBrush); painter.drawRect(bboxRect); if (h.maxHp > 0.0f) { drawHealthBar(painter, coordinates, bboxRect.left(), bboxRect.bottom() + 1.0, bboxRect.width(), h.hp / h.maxHp, f.isEnemy); } 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, const WorldCoordinates& coordinates) { const float forward = getShipForwardExtentPx(coordinates); m_sim->getAdmin().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 = coordinates.worldToWidget(pos.value); drawShipBody(painter, coordinates, center, facing.radians, it->second.fill, it->second.outline); if (h.maxHp > 0.0f) { const qreal barW = static_cast(forward) * 2.0; const qreal barX = center.x() - static_cast(forward); const qreal barY = center.y() + static_cast(forward) + 1.0; drawHealthBar(painter, coordinates, barX, barY, barW, h.hp / h.maxHp, fac.isEnemy); } if (m_selectedEntity.has_value() && *m_selectedEntity == e) { const qreal radius = static_cast(coordinates.getTilePx()) * 0.55; painter.setPen(QPen(QColor(255, 255, 0), 2)); painter.setBrush(Qt::NoBrush); painter.drawEllipse(center, radius, radius); } }); } void ArenaView::drawDebugSensorRanges(QPainter& painter, const WorldCoordinates& coordinates) { m_sim->getAdmin().forEach( [&](entt::entity /*e*/, const ShipIdentityComponent& si, const PositionComponent& pos, const SensorRangeComponent& sensor) { const std::map::const_iterator it = m_visuals->ships.find(si.schematicId); if (it == m_visuals->ships.end()) { return; } drawSensorRange(painter, coordinates, coordinates.worldToWidget(pos.value), sensor.value_tiles, it->second.outline); }); } void ArenaView::drawDebugTargetLines(QPainter& painter, const WorldCoordinates& coordinates) { // Draw a thin translucent line from a ship to a target, colored by the ship's // team to match the per-side HQ/station colors used elsewhere in the arena // (team 1 player, team 2 enemy). Shared by the attack, repair and salvage lines. const std::function drawTargetLine = [&](bool isEnemy, const QVector2D& from, const QVector2D& to) { const BuildingType visType = isEnemy ? BuildingType::EnemyDefenceStation : BuildingType::PlayerDefenceStation; const std::map::const_iterator it = m_visuals->buildings.find(visType); if (it == m_visuals->buildings.end()) { return; } QColor lineColor = it->second.fill; lineColor.setAlpha(128); painter.setPen(QPen(lineColor, 1)); painter.drawLine(coordinates.worldToWidget(from), coordinates.worldToWidget(to)); }; m_sim->getAdmin().forEach( [&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/, const PositionComponent& pos, const FactionComponent& fac, const AttackBehavior& attack) { if (!attack.currentTarget.has_value()) { return; } const std::optional targetPos = entityPosition(*attack.currentTarget); if (!targetPos.has_value()) { return; } drawTargetLine(fac.isEnemy, pos.value, *targetPos); }); m_sim->getAdmin().forEach( [&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/, const PositionComponent& pos, const FactionComponent& fac, const RepairBehavior& repair) { if (!repair.currentTarget.has_value()) { return; } const std::optional targetPos = entityPosition(*repair.currentTarget); if (!targetPos.has_value()) { return; } drawTargetLine(fac.isEnemy, pos.value, *targetPos); }); m_sim->getAdmin().forEach( [&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/, const PositionComponent& pos, const FactionComponent& fac, const SalvageScrapBehavior& salvage) { if (!salvage.debrisTarget.has_value()) { return; } drawTargetLine(fac.isEnemy, pos.value, *salvage.debrisTarget); }); } void ArenaView::drawBeams(QPainter& painter, const WorldCoordinates& coordinates) { 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; } QColor color = m_visuals->beams.weaponColor; switch (beam.event.kind) { case BeamKind::Weapon: color = m_visuals->beams.weaponColor; break; case BeamKind::Repair: color = m_visuals->beams.repairColor; break; case BeamKind::Salvage: color = m_visuals->beams.salvageColor; break; } painter.setPen(QPen(color, m_visuals->beams.widthPx)); painter.drawLine(coordinates.worldToWidget(*shooterPos), coordinates.worldToWidget(*targetPos + beam.targetOffset)); } }