allow to inspect balancing arena
This commit is contained in:
@@ -256,6 +256,7 @@ void ArenaSimulation::tick()
|
||||
// Combat resolution (tick step 8).
|
||||
std::vector<FireEvent> fireEvents;
|
||||
m_combatSystem->tick(m_currentTick, *m_shipSystem, *m_buildingSystem, fireEvents);
|
||||
m_fireEvents.insert(m_fireEvents.end(), fireEvents.begin(), fireEvents.end());
|
||||
m_combatSystem->applyPendingDamage(m_currentTick, *m_shipSystem, *m_buildingSystem);
|
||||
|
||||
// Deaths (tick step 9, simplified).
|
||||
@@ -375,6 +376,57 @@ void ArenaSimulation::tickDeaths()
|
||||
}
|
||||
}
|
||||
|
||||
void ArenaSimulation::tickOnce()
|
||||
{
|
||||
if (!m_finished)
|
||||
{
|
||||
tick();
|
||||
updateStatus();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<FireEvent> ArenaSimulation::drainFireEvents()
|
||||
{
|
||||
std::vector<FireEvent> result;
|
||||
result.swap(m_fireEvents);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ArenaSimulation::isFinished() const
|
||||
{
|
||||
return m_finished;
|
||||
}
|
||||
|
||||
int ArenaSimulation::winnerTeam() const
|
||||
{
|
||||
return m_winnerTeam;
|
||||
}
|
||||
|
||||
Tick ArenaSimulation::currentTick() const
|
||||
{
|
||||
return m_currentTick;
|
||||
}
|
||||
|
||||
const ArenaConfig& ArenaSimulation::arenaConfig() const
|
||||
{
|
||||
return m_arenaConfig;
|
||||
}
|
||||
|
||||
const BuildingSystem& ArenaSimulation::buildings() const
|
||||
{
|
||||
return *m_buildingSystem;
|
||||
}
|
||||
|
||||
const ShipSystem& ArenaSimulation::ships() const
|
||||
{
|
||||
return *m_shipSystem;
|
||||
}
|
||||
|
||||
const ScrapSystem& ArenaSimulation::scraps() const
|
||||
{
|
||||
return *m_scrapSystem;
|
||||
}
|
||||
|
||||
void ArenaSimulation::updateStatus()
|
||||
{
|
||||
ArenaStatus newStatus;
|
||||
|
||||
@@ -51,7 +51,18 @@ public:
|
||||
void run();
|
||||
void requestStop();
|
||||
|
||||
void tickOnce();
|
||||
std::vector<FireEvent> drainFireEvents();
|
||||
|
||||
ArenaStatus status() const;
|
||||
bool isFinished() const;
|
||||
int winnerTeam() const;
|
||||
Tick currentTick() const;
|
||||
|
||||
const ArenaConfig& arenaConfig() const;
|
||||
const BuildingSystem& buildings() const;
|
||||
const ShipSystem& ships() const;
|
||||
const ScrapSystem& scraps() const;
|
||||
|
||||
private:
|
||||
EntityId allocateId();
|
||||
@@ -81,6 +92,8 @@ private:
|
||||
int m_winnerTeam;
|
||||
std::atomic<bool> m_stopRequested;
|
||||
|
||||
std::vector<FireEvent> m_fireEvents;
|
||||
|
||||
mutable std::mutex m_statusMutex;
|
||||
ArenaStatus m_status;
|
||||
};
|
||||
|
||||
321
src/balancing/ArenaView.cpp
Normal file
321
src/balancing/ArenaView.cpp
Normal file
@@ -0,0 +1,321 @@
|
||||
#include "ArenaView.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPoint>
|
||||
|
||||
#include "ArenaSimulation.h"
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "Scrap.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "Ship.h"
|
||||
#include "ShipSystem.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
ShipRole shipRole(const Ship& ship)
|
||||
{
|
||||
if (ship.isEnemy) { return ShipRole::Enemy; }
|
||||
if (ship.cargo.has_value()) { return ShipRole::Salvage; }
|
||||
if (ship.repairTool.has_value()) { 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;
|
||||
const Building* tBld = m_sim->buildings().findBuilding(fe.target);
|
||||
if (tBld)
|
||||
{
|
||||
const int shorter = std::min(tBld->footprint.width(),
|
||||
tBld->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);
|
||||
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(EntityId id) const
|
||||
{
|
||||
for (const Ship& ship : m_sim->ships().allShips())
|
||||
{
|
||||
if (ship.id == id)
|
||||
{
|
||||
return ship.position;
|
||||
}
|
||||
}
|
||||
const Building* bldg = m_sim->buildings().findBuilding(id);
|
||||
if (bldg)
|
||||
{
|
||||
return QVector2D(
|
||||
bldg->anchor.x() + bldg->footprint.width() * 0.5f,
|
||||
bldg->anchor.y() + bldg->footprint.height() * 0.5f);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 Scrap& scrap : m_sim->scraps().allScraps())
|
||||
{
|
||||
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::drawShips(QPainter& painter)
|
||||
{
|
||||
for (const Ship& ship : m_sim->ships().allShips())
|
||||
{
|
||||
const ShipRole role = shipRole(ship);
|
||||
const std::map<ShipRole, ShipVisuals>::const_iterator it =
|
||||
m_visuals->ships.find(role);
|
||||
if (it == m_visuals->ships.end()) { continue; }
|
||||
|
||||
const QPointF center = worldToWidget(ship.position);
|
||||
const QVector2D vel = ship.velocity;
|
||||
const QVector2D dir = (vel.length() > 0.0001f)
|
||||
? vel.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));
|
||||
}
|
||||
}
|
||||
79
src/balancing/ArenaView.h
Normal file
79
src/balancing/ArenaView.h
Normal file
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QTimer>
|
||||
#include <QVector2D>
|
||||
|
||||
#include "EntityId.h"
|
||||
#include "FireEvent.h"
|
||||
#include "Tick.h"
|
||||
#include "TickDriver.h"
|
||||
#include "VisualsConfig.h"
|
||||
|
||||
class ArenaSimulation;
|
||||
class QPainter;
|
||||
|
||||
class ArenaView : public QOpenGLWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
void setGameSpeed(double multiplier);
|
||||
double gameSpeed() const;
|
||||
void togglePause();
|
||||
|
||||
signals:
|
||||
void speedChanged(double multiplier);
|
||||
void finished();
|
||||
|
||||
protected:
|
||||
void paintGL() override;
|
||||
|
||||
private slots:
|
||||
void onFrame();
|
||||
|
||||
private:
|
||||
void drawTiles(QPainter& painter);
|
||||
void drawBuildings(QPainter& painter);
|
||||
void drawScrap(QPainter& painter);
|
||||
void drawShips(QPainter& painter);
|
||||
void drawBeams(QPainter& painter);
|
||||
|
||||
float tilePx() const;
|
||||
QPointF worldToWidget(QVector2D worldPos) const;
|
||||
QPointF tileToWidget(QPoint tile) const;
|
||||
QRectF tileRect(QPoint tile) const;
|
||||
|
||||
std::optional<QVector2D> entityPosition(EntityId id) const;
|
||||
|
||||
struct ActiveBeam
|
||||
{
|
||||
FireEvent event;
|
||||
qint64 emittedWallMs;
|
||||
QVector2D targetOffset;
|
||||
};
|
||||
|
||||
static constexpr qint64 kBeamLifetimeMs = 300;
|
||||
|
||||
ArenaSimulation* m_sim;
|
||||
const VisualsConfig* m_visuals;
|
||||
|
||||
TickDriver m_tickDriver;
|
||||
QElapsedTimer m_frameTimer;
|
||||
qint64 m_wallMs;
|
||||
std::mt19937 m_rng;
|
||||
double m_gameSpeedMultiplier;
|
||||
double m_prevNonZeroSpeed;
|
||||
|
||||
QTimer* m_renderTimer;
|
||||
|
||||
std::vector<ActiveBeam> m_activeBeams;
|
||||
bool m_finishedEmitted;
|
||||
};
|
||||
@@ -30,6 +30,10 @@ void ArenaWidget::buildLayout(const std::string& arenaName)
|
||||
|
||||
titleRow->addStretch();
|
||||
|
||||
m_inspectButton = new QPushButton("Inspect", this);
|
||||
connect(m_inspectButton, &QPushButton::clicked, this, &ArenaWidget::inspectRequested);
|
||||
titleRow->addWidget(m_inspectButton);
|
||||
|
||||
m_startButton = new QPushButton("Start", this);
|
||||
connect(m_startButton, &QPushButton::clicked, this, &ArenaWidget::startRequested);
|
||||
titleRow->addWidget(m_startButton);
|
||||
@@ -72,6 +76,14 @@ void ArenaWidget::startSimulation()
|
||||
setStyleSheet("ArenaWidget { border: 2px solid #3366ff; padding: 8px; }");
|
||||
}
|
||||
|
||||
void ArenaWidget::resetToGrey()
|
||||
{
|
||||
m_running = false;
|
||||
m_wasFinished = false;
|
||||
m_startButton->setEnabled(true);
|
||||
setStyleSheet("ArenaWidget { border: 2px solid #999999; padding: 8px; }");
|
||||
}
|
||||
|
||||
void ArenaWidget::updateStatus(const ArenaStatus& status)
|
||||
{
|
||||
for (int ti = 0; ti < 2; ++ti)
|
||||
|
||||
@@ -18,9 +18,11 @@ public:
|
||||
|
||||
void updateStatus(const ArenaStatus& status);
|
||||
void startSimulation();
|
||||
void resetToGrey();
|
||||
|
||||
signals:
|
||||
void startRequested();
|
||||
void inspectRequested();
|
||||
|
||||
private:
|
||||
void buildLayout(const std::string& arenaName);
|
||||
@@ -30,6 +32,7 @@ private:
|
||||
QLabel* m_team2Header;
|
||||
QLabel* m_team1Content;
|
||||
QLabel* m_team2Content;
|
||||
QPushButton* m_inspectButton;
|
||||
QPushButton* m_startButton;
|
||||
bool m_running;
|
||||
bool m_wasFinished;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "ConfigLoader.h"
|
||||
#include "InspectWindow.h"
|
||||
#include "VisualsLoader.h"
|
||||
|
||||
BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
||||
GameConfig gameConfig,
|
||||
@@ -16,7 +18,10 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
||||
, m_configDir(configDir)
|
||||
, m_balancingConfigPath(balancingConfigPath)
|
||||
, m_nextSeed(0)
|
||||
, m_inspectWindow(nullptr)
|
||||
, m_inspectedArenaIndex(-1)
|
||||
{
|
||||
m_visuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
setWindowTitle("DotaFactory — Balancing Tool");
|
||||
resize(800, 600);
|
||||
|
||||
@@ -48,6 +53,13 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
||||
BalancingWindow::~BalancingWindow()
|
||||
{
|
||||
m_pollTimer->stop();
|
||||
if (m_inspectWindow)
|
||||
{
|
||||
m_inspectWindow->disconnect(this);
|
||||
delete m_inspectWindow;
|
||||
m_inspectWindow = nullptr;
|
||||
}
|
||||
m_inspectedSim.reset();
|
||||
stopAllArenas();
|
||||
}
|
||||
|
||||
@@ -76,6 +88,8 @@ void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig)
|
||||
|
||||
connect(entry.widget, &ArenaWidget::startRequested,
|
||||
this, [this, index]() { startArena(index); });
|
||||
connect(entry.widget, &ArenaWidget::inspectRequested,
|
||||
this, [this, index]() { inspectArena(index); });
|
||||
|
||||
m_arenas.push_back(std::move(entry));
|
||||
}
|
||||
@@ -111,6 +125,13 @@ void BalancingWindow::pollStatuses()
|
||||
entry.widget->updateStatus(status);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_inspectedSim && m_inspectedArenaIndex >= 0)
|
||||
{
|
||||
const ArenaStatus status = m_inspectedSim->status();
|
||||
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->updateStatus(status);
|
||||
}
|
||||
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
@@ -154,8 +175,93 @@ void BalancingWindow::startArena(int index)
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
void BalancingWindow::inspectArena(int index)
|
||||
{
|
||||
if (m_inspectWindow)
|
||||
{
|
||||
m_inspectWindow->disconnect(this);
|
||||
delete m_inspectWindow;
|
||||
m_inspectWindow = nullptr;
|
||||
|
||||
if (m_inspectedSim && m_inspectedArenaIndex >= 0
|
||||
&& !m_inspectedSim->isFinished())
|
||||
{
|
||||
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->resetToGrey();
|
||||
}
|
||||
m_inspectedSim.reset();
|
||||
m_inspectedArenaIndex = -1;
|
||||
}
|
||||
|
||||
ArenaEntry& entry = m_arenas[static_cast<std::size_t>(index)];
|
||||
|
||||
if (entry.worker.joinable())
|
||||
{
|
||||
entry.simulation->requestStop();
|
||||
entry.worker.join();
|
||||
}
|
||||
|
||||
m_inspectedSim = std::make_unique<ArenaSimulation>(
|
||||
m_gameConfig, entry.config, m_nextSeed++);
|
||||
m_inspectedArenaIndex = index;
|
||||
|
||||
entry.widget->resetToGrey();
|
||||
entry.widget->startSimulation();
|
||||
entry.widget->updateStatus(m_inspectedSim->status());
|
||||
|
||||
m_inspectWindow = new InspectWindow(
|
||||
m_inspectedSim.get(), &m_visuals, entry.config.name, nullptr);
|
||||
connect(m_inspectWindow, &InspectWindow::closed,
|
||||
this, &BalancingWindow::closeInspectWindow);
|
||||
|
||||
setMainControlsEnabled(false);
|
||||
m_inspectWindow->show();
|
||||
}
|
||||
|
||||
void BalancingWindow::closeInspectWindow()
|
||||
{
|
||||
if (!m_inspectWindow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_inspectWindow->disconnect(this);
|
||||
m_inspectWindow->deleteLater();
|
||||
m_inspectWindow = nullptr;
|
||||
|
||||
if (m_inspectedArenaIndex >= 0 && m_inspectedSim)
|
||||
{
|
||||
if (!m_inspectedSim->isFinished())
|
||||
{
|
||||
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->resetToGrey();
|
||||
}
|
||||
}
|
||||
|
||||
m_inspectedSim.reset();
|
||||
m_inspectedArenaIndex = -1;
|
||||
setMainControlsEnabled(true);
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
void BalancingWindow::setMainControlsEnabled(bool enabled)
|
||||
{
|
||||
m_reloadButton->setEnabled(enabled);
|
||||
m_startAllButton->setEnabled(enabled);
|
||||
for (ArenaEntry& entry : m_arenas)
|
||||
{
|
||||
for (QPushButton* btn : entry.widget->findChildren<QPushButton*>())
|
||||
{
|
||||
btn->setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BalancingWindow::updateButtons()
|
||||
{
|
||||
if (m_inspectWindow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool anyRunning = false;
|
||||
bool allRunning = true;
|
||||
for (ArenaEntry& entry : m_arenas)
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#include "ArenaSimulation.h"
|
||||
#include "BalancingConfig.h"
|
||||
#include "GameConfig.h"
|
||||
#include "VisualsConfig.h"
|
||||
|
||||
class InspectWindow;
|
||||
|
||||
class BalancingWindow : public QWidget
|
||||
{
|
||||
@@ -32,11 +35,14 @@ private slots:
|
||||
void reloadConfig();
|
||||
void startAll();
|
||||
void startArena(int index);
|
||||
void inspectArena(int index);
|
||||
void closeInspectWindow();
|
||||
|
||||
private:
|
||||
void populateArenas(const BalancingConfig& balancingConfig);
|
||||
void stopAllArenas();
|
||||
void updateButtons();
|
||||
void setMainControlsEnabled(bool enabled);
|
||||
|
||||
struct ArenaEntry
|
||||
{
|
||||
@@ -48,6 +54,7 @@ private:
|
||||
|
||||
std::vector<ArenaEntry> m_arenas;
|
||||
GameConfig m_gameConfig;
|
||||
VisualsConfig m_visuals;
|
||||
std::string m_configDir;
|
||||
std::string m_balancingConfigPath;
|
||||
unsigned int m_nextSeed;
|
||||
@@ -55,4 +62,8 @@ private:
|
||||
QPushButton* m_startAllButton;
|
||||
QScrollArea* m_scrollArea;
|
||||
QTimer* m_pollTimer;
|
||||
|
||||
InspectWindow* m_inspectWindow;
|
||||
int m_inspectedArenaIndex;
|
||||
std::unique_ptr<ArenaSimulation> m_inspectedSim;
|
||||
};
|
||||
|
||||
@@ -2,8 +2,12 @@ SET(HDRS
|
||||
${HDRS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaWidget.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaSimulation.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaView.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -12,7 +16,10 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaWidget.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaSimulation.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaView.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
187
src/balancing/InspectWindow.cpp
Normal file
187
src/balancing/InspectWindow.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
#include "InspectWindow.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QCloseEvent>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QSignalMapper>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "ArenaView.h"
|
||||
|
||||
const double InspectWindow::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 4.0 };
|
||||
const int InspectWindow::kSpeedCount = 5;
|
||||
|
||||
InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
const std::string& arenaName, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_sim(sim)
|
||||
{
|
||||
setWindowTitle(QString("Inspect \u2014 %1").arg(QString::fromStdString(arenaName)));
|
||||
resize(900, 700);
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// Header: arena name + speed buttons
|
||||
{
|
||||
QWidget* header = new QWidget(this);
|
||||
QHBoxLayout* headerLayout = new QHBoxLayout(header);
|
||||
headerLayout->setContentsMargins(8, 4, 8, 4);
|
||||
headerLayout->setSpacing(8);
|
||||
|
||||
QLabel* nameLabel = new QLabel(QString::fromStdString(arenaName), header);
|
||||
QFont nameFont = nameLabel->font();
|
||||
nameFont.setBold(true);
|
||||
nameFont.setPointSize(nameFont.pointSize() + 2);
|
||||
nameLabel->setFont(nameFont);
|
||||
headerLayout->addWidget(nameLabel);
|
||||
|
||||
headerLayout->addStretch();
|
||||
|
||||
const char* labels[] = { "0x", "0.5x", "1x", "2x", "4x" };
|
||||
QSignalMapper* mapper = new QSignalMapper(this);
|
||||
for (int i = 0; i < kSpeedCount; ++i)
|
||||
{
|
||||
QPushButton* btn = new QPushButton(labels[i], header);
|
||||
btn->setCheckable(true);
|
||||
btn->setChecked(i == 2);
|
||||
headerLayout->addWidget(btn);
|
||||
m_speedButtons.push_back(btn);
|
||||
mapper->setMapping(btn, i);
|
||||
connect(btn, &QPushButton::clicked,
|
||||
mapper, qOverload<>(&QSignalMapper::map));
|
||||
}
|
||||
connect(mapper, qOverload<int>(&QSignalMapper::mapped),
|
||||
this, &InspectWindow::onSpeedButton);
|
||||
|
||||
header->setFixedHeight(header->sizeHint().height());
|
||||
mainLayout->addWidget(header);
|
||||
}
|
||||
|
||||
// Arena view (center, stretch)
|
||||
m_arenaView = new ArenaView(sim, visuals, this);
|
||||
mainLayout->addWidget(m_arenaView, 1);
|
||||
|
||||
connect(m_arenaView, &ArenaView::speedChanged,
|
||||
this, &InspectWindow::onSpeedChanged);
|
||||
|
||||
// Info panel (bottom)
|
||||
{
|
||||
QWidget* infoPanel = new QWidget(this);
|
||||
QHBoxLayout* infoLayout = new QHBoxLayout(infoPanel);
|
||||
infoLayout->setContentsMargins(8, 4, 8, 4);
|
||||
infoLayout->setSpacing(16);
|
||||
|
||||
QVBoxLayout* team1Layout = new QVBoxLayout();
|
||||
m_team1Header = new QLabel(infoPanel);
|
||||
QFont headerFont = m_team1Header->font();
|
||||
headerFont.setBold(true);
|
||||
m_team1Header->setFont(headerFont);
|
||||
team1Layout->addWidget(m_team1Header);
|
||||
m_team1Content = new QLabel(infoPanel);
|
||||
team1Layout->addWidget(m_team1Content);
|
||||
team1Layout->addStretch();
|
||||
infoLayout->addLayout(team1Layout);
|
||||
|
||||
QVBoxLayout* team2Layout = new QVBoxLayout();
|
||||
m_team2Header = new QLabel(infoPanel);
|
||||
m_team2Header->setFont(headerFont);
|
||||
team2Layout->addWidget(m_team2Header);
|
||||
m_team2Content = new QLabel(infoPanel);
|
||||
team2Layout->addWidget(m_team2Content);
|
||||
team2Layout->addStretch();
|
||||
infoLayout->addLayout(team2Layout);
|
||||
|
||||
mainLayout->addWidget(infoPanel);
|
||||
}
|
||||
|
||||
// Poll timer for info panel updates
|
||||
m_pollTimer = new QTimer(this);
|
||||
connect(m_pollTimer, &QTimer::timeout, this, &InspectWindow::pollStatus);
|
||||
m_pollTimer->start(100);
|
||||
|
||||
// Show initial status
|
||||
pollStatus();
|
||||
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
}
|
||||
|
||||
void InspectWindow::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
m_pollTimer->stop();
|
||||
emit closed();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void InspectWindow::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Space)
|
||||
{
|
||||
m_arenaView->togglePause();
|
||||
}
|
||||
else
|
||||
{
|
||||
QWidget::keyPressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void InspectWindow::onSpeedButton(int index)
|
||||
{
|
||||
if (index >= 0 && index < kSpeedCount)
|
||||
{
|
||||
m_arenaView->setGameSpeed(kSpeeds[index]);
|
||||
}
|
||||
}
|
||||
|
||||
void InspectWindow::onSpeedChanged(double multiplier)
|
||||
{
|
||||
for (int i = 0; i < kSpeedCount; ++i)
|
||||
{
|
||||
const bool active = (std::abs(kSpeeds[i] - multiplier) < 0.001);
|
||||
m_speedButtons[static_cast<std::size_t>(i)]->setChecked(active);
|
||||
}
|
||||
}
|
||||
|
||||
void InspectWindow::pollStatus()
|
||||
{
|
||||
const ArenaStatus status = m_sim->status();
|
||||
updateInfoPanel(status);
|
||||
}
|
||||
|
||||
void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
||||
{
|
||||
for (int ti = 0; ti < 2; ++ti)
|
||||
{
|
||||
const ArenaStatus::TeamStatus& team = status.teams[ti];
|
||||
QLabel* header = (ti == 0) ? m_team1Header : m_team2Header;
|
||||
QLabel* content = (ti == 0) ? m_team1Content : m_team2Content;
|
||||
|
||||
if (status.finished && status.winnerTeam == ti)
|
||||
{
|
||||
header->setText("[WON] " + QString::fromStdString(team.name));
|
||||
}
|
||||
else
|
||||
{
|
||||
header->setText(QString::fromStdString(team.name));
|
||||
}
|
||||
|
||||
QString lines;
|
||||
for (const ArenaStatus::Entry& entry : team.entries)
|
||||
{
|
||||
if (!lines.isEmpty())
|
||||
{
|
||||
lines += "\n";
|
||||
}
|
||||
lines += QString("%1/%2 %3 L%4")
|
||||
.arg(entry.surviving)
|
||||
.arg(entry.total)
|
||||
.arg(QString::fromStdString(entry.displayName))
|
||||
.arg(entry.level);
|
||||
}
|
||||
content->setText(lines);
|
||||
}
|
||||
}
|
||||
51
src/balancing/InspectWindow.h
Normal file
51
src/balancing/InspectWindow.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include "ArenaSimulation.h"
|
||||
#include "VisualsConfig.h"
|
||||
|
||||
class ArenaView;
|
||||
|
||||
class InspectWindow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
const std::string& arenaName, QWidget* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void closed();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
|
||||
private slots:
|
||||
void onSpeedButton(int index);
|
||||
void onSpeedChanged(double multiplier);
|
||||
void pollStatus();
|
||||
|
||||
private:
|
||||
void updateInfoPanel(const ArenaStatus& status);
|
||||
|
||||
ArenaSimulation* m_sim;
|
||||
ArenaView* m_arenaView;
|
||||
|
||||
std::vector<QPushButton*> m_speedButtons;
|
||||
QLabel* m_team1Header;
|
||||
QLabel* m_team2Header;
|
||||
QLabel* m_team1Content;
|
||||
QLabel* m_team2Content;
|
||||
QTimer* m_pollTimer;
|
||||
|
||||
static const double kSpeeds[];
|
||||
static const int kSpeedCount;
|
||||
};
|
||||
Reference in New Issue
Block a user