Files
dota_factory/src/balancing/ArenaView.cpp

506 lines
17 KiB
C++

#include "ArenaView.h"
#include "FactoryQueries.h"
#include <algorithm>
#include <cmath>
#include <functional>
#include <optional>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPainter>
#include <QPoint>
#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<GameSpeedChangedEvent>(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<double>(elapsed), m_gameSpeedMultiplier);
for (int i = 0; i < ticks; ++i)
{
m_sim->tickOnce();
}
}
// Emit fire events via EventManager
{
const std::vector<BeamFiredEvent> fires = m_sim->drainBeamFiredEvents();
for (const BeamFiredEvent& fe : fires)
{
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BeamFiredEvent>(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<ActiveBeam> 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<const BeamFiredEvent> event)
{
float maxRadius = 0.125f;
if (m_sim->getAdmin().isValid(event->target)
&& m_sim->getAdmin().hasAll<StationBodyComponent>(event->target))
{
const StationBodyComponent& sb = m_sim->getAdmin().get<StationBodyComponent>(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<DebrisComponent>(event->target))
{
maxRadius = 0.1f;
}
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.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<QVector2D> ArenaView::entityPosition(entt::entity entity) const
{
if (!m_sim->getAdmin().isValid(entity) || !m_sim->getAdmin().hasAll<PositionComponent>(entity))
{
return std::nullopt;
}
return m_sim->getAdmin().get<PositionComponent>(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<entt::entity> selection;
if (m_selectedEntity.has_value())
{
selection.push_back(*m_selectedEntity);
}
EventManager::getInstance()->sendEventImmediately(
std::make_shared<EntitySelectionChangedEvent>(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<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(coordinates.tileRect(cell), bv.fill);
}
const QPointF tl = coordinates.tileToWidget(b.anchor);
const QRectF bboxRect(tl.x(), tl.y(),
b.footprint.width() * static_cast<qreal>(coordinates.getTilePx()),
b.footprint.height() * static_cast<qreal>(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<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(coordinates.tileRect(cell), bv.fill);
}
const QPointF tl = coordinates.tileToWidget(sb.anchor);
const QRectF bboxRect(tl.x(), tl.y(),
sb.footprint.width() * static_cast<qreal>(coordinates.getTilePx()),
sb.footprint.height() * static_cast<qreal>(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<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 = 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<qreal>(forward) * 2.0;
const qreal barX = center.x() - static_cast<qreal>(forward);
const qreal barY = center.y() + static_cast<qreal>(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<qreal>(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<ShipIdentityComponent, PositionComponent, SensorRangeComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const SensorRangeComponent& sensor)
{
const std::map<std::string, ShipVisuals>::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<void(bool, const QVector2D&, const QVector2D&)> drawTargetLine =
[&](bool isEnemy, const QVector2D& from, const QVector2D& to)
{
const BuildingType visType = 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; }
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<ShipIdentityComponent, PositionComponent,
FactionComponent, AttackBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& fac,
const AttackBehavior& attack)
{
if (!attack.currentTarget.has_value()) { return; }
const std::optional<QVector2D> targetPos =
entityPosition(*attack.currentTarget);
if (!targetPos.has_value()) { return; }
drawTargetLine(fac.isEnemy, pos.value, *targetPos);
});
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
FactionComponent, RepairBehavior>(
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& fac,
const RepairBehavior& repair)
{
if (!repair.currentTarget.has_value()) { return; }
const std::optional<QVector2D> targetPos =
entityPosition(*repair.currentTarget);
if (!targetPos.has_value()) { return; }
drawTargetLine(fac.isEnemy, pos.value, *targetPos);
});
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
FactionComponent, SalvageScrapBehavior>(
[&](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<QVector2D> shooterPos = entityPosition(beam.event.shooter);
const std::optional<QVector2D> 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));
}
}