Files
dota_factory/src/balancing/ArenaView.cpp

359 lines
11 KiB
C++

#include "ArenaView.h"
#include <algorithm>
#include <cmath>
#include <optional>
#include <QPainter>
#include <QPoint>
#include "ArenaSimulation.h"
#include "Building.h"
#include "BuildingSystem.h"
#include "EcsComponents.h"
#include "ScrapSystem.h"
#include "Ship.h"
namespace
{
ShipRole shipRoleFromComponents(bool isEnemy, bool hasCargo, bool hasRepairTool)
{
if (isEnemy) { return ShipRole::Enemy; }
if (hasCargo) { return ShipRole::Salvage; }
if (hasRepairTool) { return ShipRole::Repair; }
return ShipRole::PlayerCombat;
}
} // 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::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();
}
}
{
const std::vector<FireEvent> fires = m_sim->drainFireEvents();
for (const FireEvent& fe : fires)
{
float maxRadius = 0.125f;
if (m_sim->admin().isValid(fe.target)
&& m_sim->admin().hasAll<StationBody>(fe.target))
{
const StationBody& sb = m_sim->admin().get<StationBody>(fe.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 = fe;
beam.emittedWallMs = m_wallMs;
beam.targetOffset = QVector2D(radius * std::cos(angle),
radius * std::sin(angle));
m_activeBeams.push_back(beam);
}
}
{
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;
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
+ ac.contestZoneWidth
+ ac.enemyBufferWidth;
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<Position>(entity))
{
return std::nullopt;
}
return m_sim->admin().get<Position>(entity).value;
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
void ArenaView::drawTiles(QPainter& painter)
{
const ArenaConfig& ac = m_sim->arenaConfig();
const int totalWidth = ac.playerBufferWidth
+ ac.contestZoneWidth
+ ac.enemyBufferWidth;
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<StationBody, Faction, Health>(
[&](entt::entity /*e*/, const StationBody& sb, const Faction& f, const Health& 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));
}
});
}
void ArenaView::drawShips(QPainter& painter)
{
m_sim->admin().forEach<ShipIdentity, Position, Velocity, Faction>(
[&](entt::entity e, const ShipIdentity& /*si*/, const Position& pos,
const Velocity& vel, const Faction& fac)
{
const bool hasCargo = m_sim->admin().hasAll<SalvageCargo>(e);
const bool hasRepair = m_sim->admin().hasAll<RepairTool>(e);
const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair);
const std::map<ShipRole, ShipVisuals>::const_iterator it =
m_visuals->ships.find(role);
if (it == m_visuals->ships.end()) { return; }
const QPointF center = worldToWidget(pos.value);
const QVector2D dir = (vel.value.length() > 0.0001f)
? vel.value.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<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);
});
}
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));
}
}