Files
dota_factory/src/ui/GameWorldView.cpp

1545 lines
49 KiB
C++

#include "GameWorldView.h"
#include <algorithm>
#include <cctype>
#include <climits>
#include <cmath>
#include <map>
#include <string>
#include <QColor>
#include <QFont>
#include <QKeyEvent>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QPen>
#include <QPolygonF>
#include <QTimer>
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingSystem.h"
#include "EntityHitTest.h"
#include "EntitySelectedEvent.h"
#include "EventManager.h"
#include "FacingComponent.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "PositionComponent.h"
#include "ScrapSystem.h"
#include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h"
#include "ShipSystem.h"
#include "Simulation.h"
#include "StationBodyComponent.h"
#include "SurfaceMask.h"
#include "Tick.h"
#include "TracePrintRequestedEvent.h"
#include "BossWaveUpdatedEvent.h"
#include "BuildingBlocksChangedEvent.h"
#include "GameSpeedChangedEvent.h"
#include "TickAdvancedEvent.h"
namespace
{
Rotation rotateClockwise(Rotation r)
{
switch (r)
{
case Rotation::North: return Rotation::East;
case Rotation::East: return Rotation::South;
case Rotation::South: return Rotation::West;
case Rotation::West: return Rotation::North;
}
return Rotation::East;
}
Rotation rotateCounterClockwise(Rotation r)
{
switch (r)
{
case Rotation::North: return Rotation::West;
case Rotation::East: return Rotation::North;
case Rotation::South: return Rotation::East;
case Rotation::West: return Rotation::South;
}
return Rotation::East;
}
QString toDisplayName(const std::string& id)
{
QString result;
bool nextUpper = true;
for (char c : id)
{
if (c == '_')
{
result += ' ';
nextUpper = true;
}
else if (nextUpper)
{
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
nextUpper = false;
}
else
{
result += c;
}
}
return result;
}
QPoint portBodyTile(QPoint portTile, Rotation direction)
{
switch (direction)
{
case Rotation::East: return portTile + QPoint(-1, 0);
case Rotation::West: return portTile + QPoint( 1, 0);
case Rotation::North: return portTile + QPoint( 0, 1);
case Rotation::South: return portTile + QPoint( 0, -1);
}
return portTile;
}
} // namespace
GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
const VisualsConfig* visuals, QWidget* parent)
: QOpenGLWidget(parent)
, m_sim(sim)
, m_config(config)
, m_visuals(visuals)
, m_wallMs(0)
, m_gameSpeedMultiplier(1.0)
, m_prevNonZeroSpeed(1.0)
, m_scrollXTiles(0.0f)
, m_ghostRotation(Rotation::East)
, m_ghostValid(false)
, m_dragging(false)
, m_demolishMode(false)
, m_demolishHoverBuildingId(kInvalidBuildingId)
, m_debugDraw(false)
, m_rng(std::random_device{}())
, m_boxSelecting(false)
, m_scrollLeft(false)
, m_scrollRight(false)
, m_gameOverShown(false)
{
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
m_renderTimer = new QTimer(this);
m_renderTimer->setInterval(16);
connect(m_renderTimer, &QTimer::timeout, this, &GameWorldView::onFrame);
m_renderTimer->start();
m_frameTimer.start();
}
void GameWorldView::initializeGL()
{
// QPainter handles all rendering; no custom GL state needed.
}
void GameWorldView::onFrame()
{
const qint64 elapsed = m_frameTimer.restart();
m_wallMs += elapsed;
// Advance simulation
{
const int ticks = m_tickDriver.advance(
static_cast<double>(elapsed), m_gameSpeedMultiplier);
for (int i = 0; i < ticks; ++i)
{
m_sim->tick();
}
}
// Drain fire events → active beams
{
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<StationBodyComponent>(fe.target))
{
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(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);
}
}
// Drain schematic drop events → toasts
{
const std::vector<SchematicDropEvent> drops =
m_sim->drainSchematicDropEvents();
for (const SchematicDropEvent& ev : drops)
{
const QString name = toDisplayName(ev.schematicId);
ToastEntry toast;
if (ev.isModuleSchematic)
{
toast.text = ev.wasNewUnlock
? tr("Module unlocked: ") + name
: name + tr(" production level -> ") + QString::number(ev.newLevel);
}
else
{
toast.text = ev.wasNewUnlock
? tr("Schematic unlocked: ") + name
: name + tr(" production level -> ") + QString::number(ev.newLevel);
}
toast.createdWallMs = m_wallMs;
m_toasts.push_back(toast);
}
}
// 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);
}
// Expire old toasts
{
std::vector<ToastEntry> live;
for (const ToastEntry& t : m_toasts)
{
if (m_wallMs - t.createdWallMs < kToastLifetimeMs)
{
live.push_back(t);
}
}
m_toasts = std::move(live);
}
// Apply held scroll
{
const float delta = kScrollSpeedTilesPerSec
* static_cast<float>(elapsed) / 1000.0f;
if (m_scrollLeft) { m_scrollXTiles -= delta; }
if (m_scrollRight) { m_scrollXTiles += delta; }
clampScroll();
}
// Fire events for any state that changed since the last frame
{
const Tick newTick = m_sim->currentTick();
const int newBlocks = m_sim->buildingBlocksStock();
const int newBoss = m_sim->bossWaveCounter();
const Tick newCountdown = m_sim->bossCountdownTicks();
if (newTick != m_lastTick)
{
m_lastTick = newTick;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<TickAdvancedEvent>(newTick));
}
if (newBlocks != m_lastBlocks)
{
m_lastBlocks = newBlocks;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BuildingBlocksChangedEvent>(newBlocks));
}
if (newBoss != m_lastBossCounter || newCountdown != m_lastBossCountdown)
{
m_lastBossCounter = newBoss;
m_lastBossCountdown = newCountdown;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BossWaveUpdatedEvent>(newBoss, newCountdown));
}
}
// Game over check
if (m_sim->isGameOver() && !m_gameOverShown)
{
m_gameOverShown = true;
m_gameSpeedMultiplier = 0.0;
emit gameOver();
}
update();
}
void GameWorldView::paintGL()
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, false);
drawTiles(painter);
drawBuildings(painter);
drawStations(painter);
drawBeltItems(painter);
drawScrap(painter);
if (m_debugDraw)
{
drawDebugSensorRanges(painter);
drawDebugOverlay(painter);
}
drawShips(painter);
drawBeams(painter);
drawOverlays(painter);
drawScreenSpace(painter);
}
// ---------------------------------------------------------------------------
// Coordinate helpers
// ---------------------------------------------------------------------------
float GameWorldView::tilePx() const
{
if (m_config->world.heightTiles <= 0) { return 1.0f; }
return static_cast<float>(height()) / static_cast<float>(m_config->world.heightTiles);
}
float GameWorldView::viewportWidthTiles() const
{
return static_cast<float>(width()) / tilePx();
}
QPointF GameWorldView::worldToWidget(QVector2D worldPos) const
{
return QPointF(
static_cast<qreal>((worldPos.x() - m_scrollXTiles) * tilePx()),
static_cast<qreal>(worldPos.y() * tilePx()));
}
QPointF GameWorldView::tileToWidget(QPoint tile) const
{
return worldToWidget(QVector2D(static_cast<float>(tile.x()),
static_cast<float>(tile.y())));
}
QPoint GameWorldView::widgetToTile(QPoint widgetPt) const
{
const float wx = static_cast<float>(widgetPt.x()) / tilePx() + m_scrollXTiles;
const float wy = static_cast<float>(widgetPt.y()) / tilePx();
return QPoint(static_cast<int>(std::floor(wx)), static_cast<int>(std::floor(wy)));
}
QVector2D GameWorldView::widgetToWorld(QPoint widgetPt) const
{
const float wx = static_cast<float>(widgetPt.x()) / tilePx() + m_scrollXTiles;
const float wy = static_cast<float>(widgetPt.y()) / tilePx();
return QVector2D(wx, wy);
}
QRectF GameWorldView::tileRect(QPoint tile) const
{
const QPointF tl = tileToWidget(tile);
return QRectF(tl.x(), tl.y(),
static_cast<qreal>(tilePx()), static_cast<qreal>(tilePx()));
}
QRect GameWorldView::viewportRect() const
{
const int left = static_cast<int>(std::floor(m_scrollXTiles)) - 1;
const int top = 0;
const int right = static_cast<int>(std::ceil(m_scrollXTiles + viewportWidthTiles())) + 1;
const int bottom = m_config->world.heightTiles;
return QRect(left, top, right - left, bottom - top);
}
float GameWorldView::asteroidLeftEdge() const
{
float leftX = -static_cast<float>(m_config->world.regions.asteroidWidth_tiles);
for (const Building& b : m_sim->buildings().allBuildings())
{
for (const QPoint& cell : b.bodyCells)
{
if (static_cast<float>(cell.x()) < leftX)
{
leftX = static_cast<float>(cell.x());
}
}
}
return leftX;
}
float GameWorldView::enemyStationRightEdge() const
{
float rightX = static_cast<float>(m_config->world.regions.playerBufferWidth_tiles
+ m_config->world.regions.contestZoneWidth_tiles);
m_sim->admin().forEach<StationBodyComponent, FactionComponent>(
[&rightX](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f)
{
if (!f.isEnemy) { return; }
for (const QPoint& cell : sb.bodyCells)
{
const float cx = static_cast<float>(cell.x() + 1);
if (cx > rightX) { rightX = cx; }
}
});
return rightX;
}
void GameWorldView::clampScroll()
{
const float leftBound = asteroidLeftEdge();
const float rightBound = enemyStationRightEdge() - viewportWidthTiles();
m_scrollXTiles = std::max(leftBound, std::min(m_scrollXTiles, rightBound));
}
// ---------------------------------------------------------------------------
// Placement helpers
// ---------------------------------------------------------------------------
const BuildingDef* GameWorldView::findBuildingDef(BuildingType type) const
{
for (const BuildingDef& def : m_config->buildings.buildings)
{
if (def.type == type) { return &def; }
}
return nullptr;
}
bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
Rotation rot) const
{
const BuildingDef* def = findBuildingDef(type);
if (!def) { return false; }
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rot);
bool anyOccupied = false;
for (const QPoint& relCell : parsed.bodyCells)
{
const QPoint worldCell = anchor + relCell;
// Terrain check: S cells must be space (x >= 0), A cells must be asteroid (x < 0)
bool isShipDock = false;
for (const QPoint& dock : parsed.shipDockCells)
{
if (dock == relCell)
{
isShipDock = true;
break;
}
}
if (isShipDock && worldCell.x() < 0) { return false; }
if (!isShipDock && worldCell.x() >= 0) { return false; }
if (m_sim->buildings().isTileOccupied(worldCell)) { anyOccupied = true; }
}
if (anyOccupied)
{
return m_sim->buildings().findRotateInPlaceTarget(type, anchor, rot).has_value();
}
return true;
}
BuildingId GameWorldView::buildingAtTile(QPoint tile) const
{
for (const Building& b : m_sim->buildings().allBuildings())
{
for (const QPoint& cell : b.bodyCells)
{
if (cell == tile)
{
return b.id;
}
}
}
return kInvalidBuildingId;
}
BuildingId GameWorldView::siteAtTile(QPoint tile) const
{
for (const ConstructionSite& s : m_sim->buildings().allSites())
{
for (const QPoint& cell : s.bodyCells)
{
if (cell == tile)
{
return s.id;
}
}
}
return kInvalidBuildingId;
}
std::optional<QVector2D> GameWorldView::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;
}
void GameWorldView::stepSpeed(int delta)
{
const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
const int kCount = 5;
int current = 2;
for (int i = 0; i < kCount; ++i)
{
if (std::abs(kSpeeds[i] - m_gameSpeedMultiplier) < 0.001)
{
current = i;
break;
}
}
const int next = std::max(0, std::min(kCount - 1, current + delta));
setGameSpeed(kSpeeds[next]);
}
void GameWorldView::placeBlueprintAtTile(QPoint center)
{
const Blueprint& bp = *m_blueprintMode;
for (const BlueprintBuilding& bb : bp.buildings)
{
if (!isValidPlacement(bb.type, center + bb.offset, bb.rotation)) { return; }
}
// Cost only applies to buildings that are genuinely new (not rotate-in-place).
int totalCost = 0;
for (const BlueprintBuilding& bb : bp.buildings)
{
if (m_sim->buildings().findRotateInPlaceTarget(
bb.type, center + bb.offset, bb.rotation).has_value())
{
continue;
}
const BuildingDef* def = findBuildingDef(bb.type);
if (def) { totalCost += def->cost; }
}
if (m_sim->buildingBlocksStock() < totalCost) { return; }
for (const BlueprintBuilding& bb : bp.buildings)
{
const QPoint anchor = center + bb.offset;
const std::optional<BuildingId> rotateTarget =
m_sim->buildings().findRotateInPlaceTarget(bb.type, anchor, bb.rotation);
if (rotateTarget.has_value())
{
m_sim->buildings().rotateInPlace(*rotateTarget, bb.rotation);
continue;
}
const BuildingId id = m_sim->tryPlaceBuilding(bb.type, anchor, bb.rotation);
if (id == kInvalidBuildingId) { continue; }
if (!bb.recipeId.empty())
{
if (bb.type == BuildingType::Shipyard)
{
if (m_sim->isSchematicUnlocked(bb.recipeId))
{
m_sim->buildings().setRecipe(id, bb.recipeId);
}
}
else
{
m_sim->buildings().setRecipe(id, bb.recipeId);
}
}
if (bb.shipLayout.has_value())
{
m_sim->buildings().setShipLayout(id, *bb.shipLayout);
}
}
}
void GameWorldView::placeAtTile(QPoint tile)
{
if (!m_builderType.has_value())
{
return;
}
const BuildingType type = *m_builderType;
if (!isValidPlacement(type, tile, m_ghostRotation))
{
return;
}
const std::optional<BuildingId> rotateTarget =
m_sim->buildings().findRotateInPlaceTarget(type, tile, m_ghostRotation);
if (rotateTarget.has_value())
{
m_sim->buildings().rotateInPlace(*rotateTarget, m_ghostRotation);
return;
}
if (type == BuildingType::Belt)
{
if (m_beltDragTiles.count(tile) > 0)
{
return;
}
if (!m_sim->buildings().isTileOccupied(tile))
{
const BuildingId id = m_sim->tryPlaceBuilding(
type, tile, m_ghostRotation);
if (id != kInvalidBuildingId)
{
m_beltDragTiles.insert(tile);
}
}
}
else if (type == BuildingType::Splitter
|| type == BuildingType::TunnelEntry
|| type == BuildingType::TunnelExit)
{
if (!m_sim->buildings().isTileOccupied(tile))
{
const BuildingId id = m_sim->tryPlaceBuilding(type, tile, m_ghostRotation);
if (id != kInvalidBuildingId)
{
if (type == BuildingType::TunnelEntry)
{
m_builderType = BuildingType::TunnelExit;
}
else if (type == BuildingType::TunnelExit)
{
m_builderType = BuildingType::TunnelEntry;
}
}
}
}
else
{
m_sim->tryPlaceBuilding(type, tile, m_ghostRotation);
}
}
// ---------------------------------------------------------------------------
// Port glyph helper
// ---------------------------------------------------------------------------
void GameWorldView::drawPortGlyph(QPainter& painter, QPoint bodyTile,
Rotation direction, const QColor& color)
{
const float px = tilePx();
const QRectF tr = tileRect(bodyTile);
const QPointF center(tr.x() + static_cast<qreal>(px) * 0.5,
tr.y() + static_cast<qreal>(px) * 0.5);
QPointF offset;
const char* ch;
switch (direction)
{
case Rotation::East: offset = QPointF(px * 0.25f, 0); ch = ">"; break;
case Rotation::West: offset = QPointF(-px * 0.25f, 0); ch = "<"; break;
case Rotation::North: offset = QPointF(0, -px * 0.25f); ch = "^"; break;
case Rotation::South: offset = QPointF(0, px * 0.25f); ch = "v"; break;
default: return;
}
const qreal half = static_cast<qreal>(px) * 0.3;
const QPointF pos = center + offset;
const QRectF textRect(pos.x() - half, pos.y() - half, half * 2.0, half * 2.0);
QFont f = painter.font();
f.setPixelSize(std::max(6, static_cast<int>(px * 0.4f)));
painter.setFont(f);
painter.setPen(color);
painter.drawText(textRect, Qt::AlignCenter, QString::fromLatin1(ch));
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
void GameWorldView::drawTiles(QPainter& painter)
{
const int leftTile = static_cast<int>(std::floor(m_scrollXTiles)) - 1;
const int rightTile = leftTile + static_cast<int>(std::ceil(viewportWidthTiles())) + 2;
const int bottomTile = m_config->world.heightTiles;
painter.setPen(Qt::NoPen);
for (int x = leftTile; x <= rightTile; ++x)
{
const QColor& fill = (x < 0)
? m_visuals->asteroid.fill
: m_visuals->space.fill;
for (int y = 0; y < bottomTile; ++y)
{
painter.fillRect(tileRect(QPoint(x, y)), fill);
}
}
}
void GameWorldView::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);
}
for (const Port& port : b.outputPorts)
{
drawPortGlyph(painter, portBodyTile(port.tile, port.direction),
port.direction, bv.outline);
}
bool selected = false;
for (BuildingId selId : m_selectedBuildingIds)
{
if (selId == b.id) { selected = true; break; }
}
if (selected)
{
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect.adjusted(-1, -1, 1, 1));
}
}
painter.setOpacity(0.5);
for (const ConstructionSite& s : m_sim->buildings().allSites())
{
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
m_visuals->buildings.find(s.type);
if (it == m_visuals->buildings.end()) { continue; }
const BuildingVisuals& bv = it->second;
for (const QPoint& cell : s.bodyCells)
{
painter.fillRect(tileRect(cell), bv.fill);
}
const QPointF tl = tileToWidget(s.anchor);
const QRectF bboxRect(tl.x(), tl.y(),
s.footprint.width() * static_cast<qreal>(tilePx()),
s.footprint.height() * static_cast<qreal>(tilePx()));
painter.setPen(QPen(bv.outline, 1, Qt::DashLine));
painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect);
const BuildingDef* siteDef = findBuildingDef(s.type);
if (siteDef)
{
// Glyph + progress percentage
const Tick durationTicks = secondsToTicks(siteDef->constructionTimeSeconds);
int pct = 0;
if (s.completesAt > 0 && durationTicks > 0)
{
const Tick elapsed = m_sim->currentTick()
- (s.completesAt - durationTicks);
pct = static_cast<int>(
std::max(Tick(0), std::min(durationTicks, elapsed))
* 100 / durationTicks);
}
const QString pctText = QString::number(pct) + "%";
painter.setPen(bv.outline);
if (!bv.glyph.isEmpty())
{
const QRectF topHalf(bboxRect.x(), bboxRect.y(),
bboxRect.width(), bboxRect.height() * 0.5);
const QRectF botHalf(bboxRect.x(),
bboxRect.y() + bboxRect.height() * 0.5,
bboxRect.width(), bboxRect.height() * 0.5);
painter.drawText(topHalf, Qt::AlignCenter, bv.glyph);
painter.drawText(botHalf, Qt::AlignCenter, pctText);
}
else
{
painter.drawText(bboxRect, Qt::AlignCenter, pctText);
}
// Port glyphs
const ParsedSurfaceMask siteMask =
parseSurfaceMask(siteDef->surfaceMask, s.rotation);
for (const Port& port : siteMask.outputPorts)
{
const QPoint absBody = s.anchor
+ portBodyTile(port.tile, port.direction);
drawPortGlyph(painter, absBody, port.direction, bv.outline);
}
}
}
painter.setOpacity(1.0);
}
void GameWorldView::drawBeltItems(QPainter& painter)
{
const float halfPx = tilePx() * 0.5f * 0.5f;
const QRect vr = viewportRect();
m_sim->belts().forEachVisualItem(vr, [&](const VisualItem& vi)
{
const std::map<std::string, ItemVisuals>::const_iterator it =
m_visuals->items.find(vi.type.id);
if (it == m_visuals->items.end()) { return; }
const QPointF center = worldToWidget(
QVector2D(static_cast<float>(vi.worldPos.x()),
static_cast<float>(vi.worldPos.y())));
const QRectF rect(center.x() - halfPx, center.y() - halfPx,
halfPx * 2, halfPx * 2);
painter.fillRect(rect, it->second.fill);
painter.setPen(QPen(it->second.outline, 1));
painter.setBrush(Qt::NoBrush);
painter.drawRect(rect);
});
}
void GameWorldView::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 GameWorldView::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(QPoint(sb.anchor.x(), sb.anchor.y()));
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 (m_selectedEntity.has_value() && *m_selectedEntity == e)
{
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect.adjusted(-2, -2, 2, 2));
}
// HP bar below footprint.
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 GameWorldView::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 (m_selectedEntity.has_value() && *m_selectedEntity == e)
{
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
painter.setBrush(Qt::NoBrush);
const qreal r = static_cast<qreal>(fwd) + 2.0;
painter.drawEllipse(center, r, r);
}
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));
}
});
}
void GameWorldView::drawDebugSensorRanges(QPainter& painter)
{
painter.setBrush(Qt::NoBrush);
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, SensorRangeComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const FacingComponent& /*facing*/,
const FactionComponent& /*fac*/, 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; }
const QPointF center = worldToWidget(pos.value);
const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
* static_cast<qreal>(tilePx());
painter.setPen(QPen(it->second.outline, 1));
painter.drawEllipse(center, radiusPx, radiusPx);
});
}
void GameWorldView::drawDebugOverlay(QPainter& painter)
{
painter.resetTransform();
const QString line1 = tr("Accumulated Threat Level: %1")
.arg(m_sim->threatLevel(), 0, 'f', 1);
const QString line2 = tr("Time until Wave: %1s")
.arg(ticksToSeconds(m_sim->normalGapRemainingTicks()), 0, 'f', 1);
QFont font = painter.font();
font.setPointSize(m_visuals->toast.fontSize);
painter.setFont(font);
const QFontMetrics fm = painter.fontMetrics();
const int lineH = fm.height();
const int padding = 8;
const int spacing = 4;
const int textW = std::max(fm.horizontalAdvance(line1),
fm.horizontalAdvance(line2));
const int bgW = textW + padding * 2;
const int bgH = lineH * 2 + spacing + padding * 2;
const QRect bgRect(padding, padding, bgW, bgH);
painter.fillRect(bgRect, QColor(0, 0, 0, 160));
painter.setPen(Qt::white);
const QRect textRect1(padding * 2, padding + padding, textW, lineH);
const QRect textRect2(padding * 2, textRect1.bottom() + spacing, textW, lineH);
painter.drawText(textRect1, Qt::AlignLeft | Qt::AlignVCenter, line1);
painter.drawText(textRect2, Qt::AlignLeft | Qt::AlignVCenter, line2);
}
void GameWorldView::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));
}
}
void GameWorldView::drawOverlays(QPainter& painter)
{
// Builder-mode ghost
if (m_builderType.has_value())
{
const BuildingDef* def = findBuildingDef(*m_builderType);
if (def)
{
const ParsedSurfaceMask parsed =
parseSurfaceMask(def->surfaceMask, m_ghostRotation);
const QColor& ghostColor = m_ghostValid
? m_visuals->overlays.ghostValid
: m_visuals->overlays.ghostInvalid;
for (const QPoint& cell : parsed.bodyCells)
{
painter.fillRect(tileRect(m_ghostTile + cell), ghostColor);
}
for (const Port& port : parsed.outputPorts)
{
const QPoint absBody = m_ghostTile
+ portBodyTile(port.tile, port.direction);
drawPortGlyph(painter, absBody, port.direction, Qt::white);
}
}
}
// Blueprint placement ghost
if (m_blueprintMode.has_value())
{
for (const BlueprintBuilding& bb : m_blueprintMode->buildings)
{
const QPoint anchor = m_blueprintGhostTile + bb.offset;
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
const QColor& ghostColor = valid
? m_visuals->overlays.ghostValid
: m_visuals->overlays.ghostInvalid;
const BuildingDef* def = findBuildingDef(bb.type);
if (!def) { continue; }
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, bb.rotation);
for (const QPoint& cell : parsed.bodyCells)
{
painter.fillRect(tileRect(anchor + cell), ghostColor);
}
for (const Port& port : parsed.outputPorts)
{
drawPortGlyph(painter,
anchor + portBodyTile(port.tile, port.direction),
port.direction, Qt::white);
}
}
}
// Demolish hover tint
if (m_demolishMode && m_demolishHoverBuildingId != kInvalidBuildingId)
{
const Building* b = m_sim->buildings().findBuilding(m_demolishHoverBuildingId);
if (b)
{
for (const QPoint& cell : b->bodyCells)
{
painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint);
}
}
}
// Box-select rectangle
if (m_boxSelecting)
{
const QPoint tl(std::min(m_boxStartTile.x(), m_boxCurrentTile.x()),
std::min(m_boxStartTile.y(), m_boxCurrentTile.y()));
const QPoint br(std::max(m_boxStartTile.x(), m_boxCurrentTile.x()) + 1,
std::max(m_boxStartTile.y(), m_boxCurrentTile.y()) + 1);
const QRectF selRect(tileToWidget(tl), tileToWidget(br));
painter.setPen(QPen(m_visuals->overlays.selectionRect, 1));
painter.setBrush(Qt::NoBrush);
painter.drawRect(selRect);
}
}
void GameWorldView::drawScreenSpace(QPainter& painter)
{
painter.resetTransform();
const int margin = 8;
const int toastW = 320;
const int toastH = 36;
const int spacing = 4;
QFont toastFont = painter.font();
toastFont.setPointSize(m_visuals->toast.fontSize);
painter.setFont(toastFont);
int y = margin;
for (const ToastEntry& toast : m_toasts)
{
const qint64 age = m_wallMs - toast.createdWallMs;
double opacity = 1.0;
if (age > kToastFadeStartMs)
{
opacity = 1.0 - static_cast<double>(age - kToastFadeStartMs)
/ static_cast<double>(kToastLifetimeMs - kToastFadeStartMs);
opacity = std::max(0.0, opacity);
}
painter.setOpacity(opacity);
const int x = width() - toastW - margin;
const QRect toastRect(x, y, toastW, toastH);
painter.fillRect(toastRect, m_visuals->toast.bg);
painter.setPen(m_visuals->toast.fg);
painter.drawText(toastRect.adjusted(8, 0, -8, 0),
Qt::AlignVCenter | Qt::AlignLeft, toast.text);
painter.setOpacity(1.0);
y += toastH + spacing;
}
}
// ---------------------------------------------------------------------------
// Input
// ---------------------------------------------------------------------------
void GameWorldView::keyPressEvent(QKeyEvent* event)
{
if (event->isAutoRepeat())
{
QOpenGLWidget::keyPressEvent(event);
return;
}
switch (event->key())
{
case Qt::Key_A:
m_scrollLeft = true;
break;
case Qt::Key_D:
m_scrollRight = true;
break;
case Qt::Key_Space:
if (m_gameSpeedMultiplier > 0.0)
{
m_prevNonZeroSpeed = m_gameSpeedMultiplier;
setGameSpeed(0.0);
}
else
{
setGameSpeed(m_prevNonZeroSpeed);
}
break;
case Qt::Key_W:
stepSpeed(+1);
break;
case Qt::Key_S:
stepSpeed(-1);
break;
case Qt::Key_E:
if (m_builderType.has_value())
{
m_ghostRotation = rotateClockwise(m_ghostRotation);
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
}
else if (m_blueprintMode.has_value())
{
for (BlueprintBuilding& bb : m_blueprintMode->buildings)
{
const BuildingDef* def = findBuildingDef(bb.type);
if (!def) { continue; }
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation);
int minX = INT_MAX, minY = INT_MAX;
for (const QPoint& cell : mask.bodyCells)
{
const QPoint abs = bb.offset + cell;
minX = std::min(minX, -abs.y());
minY = std::min(minY, abs.x());
}
bb.offset = QPoint(minX, minY);
bb.rotation = rotateClockwise(bb.rotation);
}
}
break;
case Qt::Key_Q:
if (m_builderType.has_value())
{
m_ghostRotation = rotateCounterClockwise(m_ghostRotation);
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
}
else if (m_blueprintMode.has_value())
{
for (BlueprintBuilding& bb : m_blueprintMode->buildings)
{
const BuildingDef* def = findBuildingDef(bb.type);
if (!def) { continue; }
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation);
int minX = INT_MAX, minY = INT_MAX;
for (const QPoint& cell : mask.bodyCells)
{
const QPoint abs = bb.offset + cell;
minX = std::min(minX, abs.y());
minY = std::min(minY, -abs.x());
}
bb.offset = QPoint(minX, minY);
bb.rotation = rotateCounterClockwise(bb.rotation);
}
}
break;
case Qt::Key_Escape:
emit escapeMenuRequested();
break;
case Qt::Key_Backspace:
toggleDemolishMode();
break;
case Qt::Key_M:
m_debugDraw = !m_debugDraw;
break;
case Qt::Key_L:
EventManager::getInstance()->addEvent(std::make_shared<TracePrintRequestedEvent>());
break;
default:
QOpenGLWidget::keyPressEvent(event);
break;
}
}
void GameWorldView::keyReleaseEvent(QKeyEvent* event)
{
if (event->isAutoRepeat())
{
QOpenGLWidget::keyReleaseEvent(event);
return;
}
if (event->key() == Qt::Key_A) { m_scrollLeft = false; }
if (event->key() == Qt::Key_D) { m_scrollRight = false; }
QOpenGLWidget::keyReleaseEvent(event);
}
void GameWorldView::mousePressEvent(QMouseEvent* event)
{
if (event->button() != Qt::LeftButton)
{
if (event->button() == Qt::RightButton)
{
if (m_builderType.has_value()) { exitBuilderMode(); }
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
}
return;
}
const QPoint tile = widgetToTile(event->pos());
if (m_builderType.has_value())
{
const BuildingType type = *m_builderType;
if (type == BuildingType::Belt || type == BuildingType::Splitter)
{
m_dragging = true;
m_beltDragTiles.clear();
placeAtTile(tile);
}
else
{
placeAtTile(tile);
}
}
else if (m_blueprintMode.has_value())
{
placeBlueprintAtTile(tile);
}
else if (m_demolishMode)
{
BuildingId hovered = buildingAtTile(tile);
if (hovered == kInvalidBuildingId)
{
hovered = siteAtTile(tile);
}
if (hovered != kInvalidBuildingId)
{
const Building* b = m_sim->buildings().findBuilding(hovered);
const bool isProtected = b && b->type == BuildingType::Hq;
if (!isProtected)
{
m_sim->demolish(hovered);
m_demolishHoverBuildingId = kInvalidBuildingId;
}
}
}
else
{
const QVector2D worldPos = widgetToWorld(event->pos());
const entt::entity hitEntity = entityAtWorldPos(m_sim->admin(), worldPos);
if (hitEntity != entt::null)
{
m_selectedBuildingIds.clear();
emit selectionChanged(m_selectedBuildingIds);
m_selectedEntity = hitEntity;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<EntitySelectedEvent>(hitEntity));
}
else
{
if (m_selectedEntity.has_value())
{
m_selectedEntity = std::nullopt;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<EntitySelectedEvent>(std::nullopt));
}
BuildingId id = buildingAtTile(tile);
if (id == kInvalidBuildingId)
{
id = siteAtTile(tile);
}
if (id != kInvalidBuildingId)
{
if (event->modifiers() & Qt::ControlModifier)
{
bool found = false;
std::vector<BuildingId> newSel;
for (BuildingId sel : m_selectedBuildingIds)
{
if (sel == id) { found = true; }
else { newSel.push_back(sel); }
}
if (!found) { newSel.push_back(id); }
m_selectedBuildingIds = newSel;
}
else
{
m_selectedBuildingIds = { id };
}
emit selectionChanged(m_selectedBuildingIds);
}
else
{
if (!(event->modifiers() & Qt::ControlModifier))
{
m_selectedBuildingIds.clear();
emit selectionChanged(m_selectedBuildingIds);
}
m_boxSelecting = true;
m_boxStartTile = tile;
m_boxCurrentTile = tile;
}
}
}
}
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
{
const QPoint tile = widgetToTile(event->pos());
if (m_builderType.has_value())
{
m_ghostTile = tile;
m_ghostValid = isValidPlacement(*m_builderType, tile, m_ghostRotation);
if (m_dragging)
{
placeAtTile(tile);
}
}
else if (m_blueprintMode.has_value())
{
m_blueprintGhostTile = tile;
}
else if (m_demolishMode)
{
m_demolishHoverBuildingId = buildingAtTile(tile);
}
else if (m_boxSelecting)
{
m_boxCurrentTile = tile;
}
}
void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
{
if (event->button() != Qt::LeftButton) { return; }
if (m_dragging)
{
m_dragging = false;
m_beltDragTiles.clear();
}
if (m_boxSelecting)
{
m_boxSelecting = false;
const int x0 = std::min(m_boxStartTile.x(), m_boxCurrentTile.x());
const int y0 = std::min(m_boxStartTile.y(), m_boxCurrentTile.y());
const int x1 = std::max(m_boxStartTile.x(), m_boxCurrentTile.x());
const int y1 = std::max(m_boxStartTile.y(), m_boxCurrentTile.y());
std::vector<BuildingId> boxSel;
for (const Building& b : m_sim->buildings().allBuildings())
{
for (const QPoint& cell : b.bodyCells)
{
if (cell.x() >= x0 && cell.x() <= x1
&& cell.y() >= y0 && cell.y() <= y1)
{
boxSel.push_back(b.id);
break;
}
}
}
for (const ConstructionSite& s : m_sim->buildings().allSites())
{
for (const QPoint& cell : s.bodyCells)
{
if (cell.x() >= x0 && cell.x() <= x1
&& cell.y() >= y0 && cell.y() <= y1)
{
boxSel.push_back(s.id);
break;
}
}
}
if (!(event->modifiers() & Qt::ControlModifier))
{
m_selectedBuildingIds = boxSel;
}
else
{
for (BuildingId id : boxSel)
{
bool found = false;
for (BuildingId sel : m_selectedBuildingIds)
{
if (sel == id) { found = true; break; }
}
if (!found) { m_selectedBuildingIds.push_back(id); }
}
}
emit selectionChanged(m_selectedBuildingIds);
}
}
// ---------------------------------------------------------------------------
// Slots
// ---------------------------------------------------------------------------
void GameWorldView::toggleDemolishMode()
{
if (m_demolishMode)
{
m_demolishMode = false;
m_demolishHoverBuildingId = kInvalidBuildingId;
}
else
{
if (m_builderType.has_value()) { exitBuilderMode(); }
if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
m_demolishMode = true;
}
emit demolishModeChanged(m_demolishMode);
}
void GameWorldView::enterBuilderMode(BuildingType type)
{
m_builderType = type;
m_ghostRotation = Rotation::East;
m_ghostValid = false;
m_demolishMode = false;
m_blueprintMode.reset();
emit demolishModeChanged(false);
}
void GameWorldView::enterBlueprintMode(Blueprint blueprint)
{
if (m_builderType.has_value()) { exitBuilderMode(); }
m_demolishMode = false;
emit demolishModeChanged(false);
m_blueprintGhostTile = m_ghostTile;
m_blueprintMode = std::move(blueprint);
}
void GameWorldView::exitBlueprintMode()
{
m_blueprintMode.reset();
emit blueprintModeExited();
}
void GameWorldView::exitBuilderMode()
{
m_builderType.reset();
m_beltDragTiles.clear();
m_dragging = false;
emit builderModeExited();
}
double GameWorldView::gameSpeed() const
{
return m_gameSpeedMultiplier;
}
void GameWorldView::setGameSpeed(double multiplier)
{
m_gameSpeedMultiplier = multiplier;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<GameSpeedChangedEvent>(m_gameSpeedMultiplier));
}
void GameWorldView::resetForNewGame()
{
exitBuilderMode();
exitBlueprintMode();
m_activeBeams.clear();
m_toasts.clear();
m_ghostRotation = Rotation::East;
m_ghostValid = false;
m_demolishMode = false;
m_demolishHoverBuildingId = kInvalidBuildingId;
emit demolishModeChanged(false);
m_selectedBuildingIds.clear();
m_boxSelecting = false;
m_scrollXTiles = 0.0f;
m_scrollLeft = false;
m_scrollRight = false;
m_gameOverShown = false;
m_prevNonZeroSpeed = 1.0;
m_lastTick = Tick(-1);
m_lastBlocks = -1;
m_lastBossCounter = -1;
m_lastBossCountdown = Tick(-1);
emit selectionChanged({});
setGameSpeed(1.0);
update();
}