433 lines
14 KiB
C++
433 lines
14 KiB
C++
#include "ArenaView.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <optional>
|
|
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QPoint>
|
|
|
|
#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 "GameSpeedChangedEvent.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();
|
|
|
|
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<GameSpeedChangedEvent>(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<double>(elapsed), m_gameSpeedMultiplier);
|
|
for (int i = 0; i < ticks; ++i)
|
|
{
|
|
m_sim->tickOnce();
|
|
}
|
|
}
|
|
|
|
// Emit fire events via EventManager
|
|
{
|
|
const std::vector<WeaponFiredEvent> fires = m_sim->drainWeaponFiredEvents();
|
|
for (const WeaponFiredEvent& fe : fires)
|
|
{
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<WeaponFiredEvent>(fe));
|
|
}
|
|
}
|
|
|
|
// Expire old beams
|
|
{
|
|
std::vector<ActiveBeam> 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;
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
void ArenaView::handleEvent(std::shared_ptr<const WeaponFiredEvent> event)
|
|
{
|
|
float maxRadius = 0.125f;
|
|
if (m_sim->admin().isValid(event->target)
|
|
&& m_sim->admin().hasAll<StationBodyComponent>(event->target))
|
|
{
|
|
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(event->target);
|
|
const int shorter = std::min(sb.footprint.width(),
|
|
sb.footprint.height());
|
|
maxRadius = shorter / 2.0f;
|
|
}
|
|
|
|
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
|
|
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
|
|
const float angle = angleDist(m_rng);
|
|
const float radius = radiusDist(m_rng);
|
|
|
|
ActiveBeam beam;
|
|
beam.event = *event;
|
|
beam.emittedWallMs = m_wallMs;
|
|
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);
|
|
|
|
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<float>(height()) / static_cast<float>(totalHeight);
|
|
const float pxPerTileW = static_cast<float>(width()) / static_cast<float>(totalWidth);
|
|
return std::min(pxPerTileH, pxPerTileW);
|
|
}
|
|
|
|
QPointF ArenaView::worldToWidget(QVector2D worldPos) const
|
|
{
|
|
return QPointF(
|
|
static_cast<qreal>(worldPos.x() * tilePx()),
|
|
static_cast<qreal>(worldPos.y() * tilePx()));
|
|
}
|
|
|
|
QPointF ArenaView::tileToWidget(QPoint tile) const
|
|
{
|
|
return worldToWidget(QVector2D(static_cast<float>(tile.x()),
|
|
static_cast<float>(tile.y())));
|
|
}
|
|
|
|
QRectF ArenaView::tileRect(QPoint tile) const
|
|
{
|
|
const QPointF tl = tileToWidget(tile);
|
|
return QRectF(tl.x(), tl.y(),
|
|
static_cast<qreal>(tilePx()), static_cast<qreal>(tilePx()));
|
|
}
|
|
|
|
std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const
|
|
{
|
|
if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll<PositionComponent>(entity))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
return m_sim->admin().get<PositionComponent>(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<float>(widgetPt.x()) / px,
|
|
static_cast<float>(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<EntitySelectedEvent>(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<BuildingType, BuildingVisuals>::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<qreal>(tilePx()),
|
|
b.footprint.height() * static_cast<qreal>(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<qreal>(r), static_cast<qreal>(r));
|
|
}
|
|
}
|
|
|
|
void ArenaView::drawStations(QPainter& painter)
|
|
{
|
|
m_sim->admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
|
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f, const HealthComponent& h)
|
|
{
|
|
const BuildingType visType = f.isEnemy
|
|
? BuildingType::EnemyDefenceStation
|
|
: BuildingType::PlayerDefenceStation;
|
|
const std::map<BuildingType, BuildingVisuals>::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<qreal>(tilePx()),
|
|
sb.footprint.height() * static_cast<qreal>(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<qreal>(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<qreal>(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<ShipIdentityComponent, PositionComponent, FacingComponent,
|
|
FactionComponent, HealthComponent>(
|
|
[&](entt::entity e, const ShipIdentityComponent& si,
|
|
const PositionComponent& pos, const FacingComponent& facing,
|
|
const FactionComponent& fac, const HealthComponent& h)
|
|
{
|
|
const std::map<std::string, ShipVisuals>::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<qreal>(dir.x() * fwd),
|
|
center.y() + static_cast<qreal>(dir.y() * fwd))
|
|
<< QPointF(center.x() + static_cast<qreal>(perp.x() * side - dir.x() * side),
|
|
center.y() + static_cast<qreal>(perp.y() * side - dir.y() * side))
|
|
<< QPointF(center.x() + static_cast<qreal>(-perp.x() * side - dir.x() * side),
|
|
center.y() + static_cast<qreal>(-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<qreal>(fwd) * 2.0;
|
|
const qreal barH = static_cast<qreal>(tilePx()) * 0.12;
|
|
const qreal barX = center.x() - static_cast<qreal>(fwd);
|
|
const qreal barY = center.y() + static_cast<qreal>(fwd) + 1.0;
|
|
painter.fillRect(QRectF(barX, barY, barW, barH), QColor(60, 60, 60));
|
|
painter.fillRect(QRectF(barX, barY, barW * static_cast<qreal>(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<qreal>(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<QVector2D> shooterPos = entityPosition(beam.event.shooter);
|
|
const std::optional<QVector2D> targetPos = entityPosition(beam.event.target);
|
|
if (!shooterPos.has_value() || !targetPos.has_value()) { continue; }
|
|
painter.drawLine(worldToWidget(*shooterPos),
|
|
worldToWidget(*targetPos + beam.targetOffset));
|
|
}
|
|
}
|