implement ui
This commit is contained in:
982
src/ui/GameWorldView.cpp
Normal file
982
src/ui/GameWorldView.cpp
Normal file
@@ -0,0 +1,982 @@
|
||||
#include "GameWorldView.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QMessageBox>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPen>
|
||||
#include <QPolygonF>
|
||||
#include <QTimer>
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "BeltSystem.h"
|
||||
#include "Scrap.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "Ship.h"
|
||||
#include "ShipSystem.h"
|
||||
#include "Simulation.h"
|
||||
#include "SurfaceMask.h"
|
||||
#include "Tick.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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
} // 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_demolishHoverId(kInvalidEntityId)
|
||||
, 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, SIGNAL(timeout()), this, SLOT(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)
|
||||
{
|
||||
ActiveBeam beam;
|
||||
beam.event = fe;
|
||||
beam.emittedWallMs = m_wallMs;
|
||||
m_activeBeams.push_back(beam);
|
||||
}
|
||||
}
|
||||
|
||||
// Drain blueprint drop events → toasts
|
||||
{
|
||||
const std::vector<BlueprintDropEvent> drops =
|
||||
m_sim->drainBlueprintDropEvents();
|
||||
for (const BlueprintDropEvent& ev : drops)
|
||||
{
|
||||
const QString shipName = toDisplayName(ev.blueprintId);
|
||||
ToastEntry toast;
|
||||
if (ev.wasNewUnlock)
|
||||
{
|
||||
toast.text = "Blueprint unlocked: " + shipName;
|
||||
}
|
||||
else
|
||||
{
|
||||
toast.text = shipName + " production level \u2192 "
|
||||
+ 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();
|
||||
}
|
||||
|
||||
// Emit state update for header bar / build grid
|
||||
emit stateUpdated(m_sim->currentTick(),
|
||||
m_sim->buildingBlocksStock(),
|
||||
m_gameSpeedMultiplier);
|
||||
|
||||
// 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);
|
||||
painter.translate(-static_cast<qreal>(m_scrollXTiles) * static_cast<qreal>(tilePx()), 0.0);
|
||||
|
||||
drawTiles(painter);
|
||||
drawBuildings(painter);
|
||||
drawBeltItems(painter);
|
||||
drawScrap(painter);
|
||||
drawShips(painter);
|
||||
drawBeams(painter);
|
||||
drawOverlays(painter);
|
||||
drawScreenSpace(painter);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Coordinate helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
float GameWorldView::tilePx() const
|
||||
{
|
||||
return 20.0f;
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
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);
|
||||
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
|
||||
+ m_config->world.regions.contestZoneWidth);
|
||||
for (const Building& b : m_sim->buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation)
|
||||
{
|
||||
for (const QPoint& cell : b.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);
|
||||
|
||||
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; }
|
||||
|
||||
// Occupancy check
|
||||
if (m_sim->buildings().isTileOccupied(worldCell)) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
EntityId 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 kInvalidEntityId;
|
||||
}
|
||||
|
||||
std::optional<QVector2D> GameWorldView::entityPosition(EntityId id) const
|
||||
{
|
||||
const Ship* ship = m_sim->ships().findShip(id);
|
||||
if (ship)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
void GameWorldView::stepSpeed(int delta)
|
||||
{
|
||||
const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 4.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::placeAtTile(QPoint tile)
|
||||
{
|
||||
if (!m_builderType.has_value()) { return; }
|
||||
const BuildingType type = *m_builderType;
|
||||
|
||||
if (!isValidPlacement(type, tile, m_ghostRotation)) { return; }
|
||||
|
||||
if (type == BuildingType::Belt)
|
||||
{
|
||||
if (m_beltDragTiles.count(tile) > 0) { return; }
|
||||
if (!m_sim->buildings().isTileOccupied(tile))
|
||||
{
|
||||
const EntityId id = m_sim->tryPlaceBuilding(
|
||||
type, tile, m_ghostRotation);
|
||||
if (id != kInvalidEntityId)
|
||||
{
|
||||
m_beltDragTiles.insert(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == BuildingType::Splitter)
|
||||
{
|
||||
if (!m_sim->buildings().isTileOccupied(tile))
|
||||
{
|
||||
m_sim->tryPlaceBuilding(type, tile, m_ghostRotation);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_sim->tryPlaceBuilding(type, tile, m_ghostRotation);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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);
|
||||
}
|
||||
|
||||
bool selected = false;
|
||||
for (EntityId selId : m_selectedIds)
|
||||
{
|
||||
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);
|
||||
}
|
||||
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 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 GameWorldView::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 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));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Demolish hover tint
|
||||
if (m_demolishMode && m_demolishHoverId != kInvalidEntityId)
|
||||
{
|
||||
const Building* b = m_sim->buildings().findBuilding(m_demolishHoverId);
|
||||
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);
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Q:
|
||||
if (m_builderType.has_value())
|
||||
{
|
||||
m_ghostRotation = rotateCounterClockwise(m_ghostRotation);
|
||||
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Escape:
|
||||
if (m_builderType.has_value())
|
||||
{
|
||||
exitBuilderMode();
|
||||
}
|
||||
else if (m_demolishMode)
|
||||
{
|
||||
m_demolishMode = false;
|
||||
m_demolishHoverId = kInvalidEntityId;
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Backspace:
|
||||
if (m_demolishMode)
|
||||
{
|
||||
m_demolishMode = false;
|
||||
m_demolishHoverId = kInvalidEntityId;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_builderType.has_value()) { exitBuilderMode(); }
|
||||
m_demolishMode = true;
|
||||
}
|
||||
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 && m_builderType.has_value())
|
||||
{
|
||||
exitBuilderMode();
|
||||
}
|
||||
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_demolishMode)
|
||||
{
|
||||
const EntityId hovered = buildingAtTile(tile);
|
||||
if (hovered != kInvalidEntityId)
|
||||
{
|
||||
const Building* b = m_sim->buildings().findBuilding(hovered);
|
||||
if (b && b->type != BuildingType::Hq
|
||||
&& b->type != BuildingType::PlayerDefenceStation)
|
||||
{
|
||||
m_sim->buildings().demolish(hovered);
|
||||
m_demolishHoverId = kInvalidEntityId;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const EntityId id = buildingAtTile(tile);
|
||||
if (id != kInvalidEntityId)
|
||||
{
|
||||
if (event->modifiers() & Qt::ControlModifier)
|
||||
{
|
||||
bool found = false;
|
||||
std::vector<EntityId> newSel;
|
||||
for (EntityId sel : m_selectedIds)
|
||||
{
|
||||
if (sel == id) { found = true; }
|
||||
else { newSel.push_back(sel); }
|
||||
}
|
||||
if (!found) { newSel.push_back(id); }
|
||||
m_selectedIds = newSel;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_selectedIds = { id };
|
||||
}
|
||||
emit selectionChanged(m_selectedIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!(event->modifiers() & Qt::ControlModifier))
|
||||
{
|
||||
m_selectedIds.clear();
|
||||
emit selectionChanged(m_selectedIds);
|
||||
}
|
||||
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_demolishMode)
|
||||
{
|
||||
m_demolishHoverId = 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<EntityId> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(event->modifiers() & Qt::ControlModifier))
|
||||
{
|
||||
m_selectedIds = boxSel;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (EntityId id : boxSel)
|
||||
{
|
||||
bool found = false;
|
||||
for (EntityId sel : m_selectedIds)
|
||||
{
|
||||
if (sel == id) { found = true; break; }
|
||||
}
|
||||
if (!found) { m_selectedIds.push_back(id); }
|
||||
}
|
||||
}
|
||||
emit selectionChanged(m_selectedIds);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void GameWorldView::enterBuilderMode(BuildingType type)
|
||||
{
|
||||
m_builderType = type;
|
||||
m_ghostRotation = Rotation::East;
|
||||
m_ghostValid = false;
|
||||
m_demolishMode = false;
|
||||
}
|
||||
|
||||
void GameWorldView::exitBuilderMode()
|
||||
{
|
||||
m_builderType.reset();
|
||||
m_beltDragTiles.clear();
|
||||
m_dragging = false;
|
||||
emit builderModeExited();
|
||||
}
|
||||
|
||||
void GameWorldView::setGameSpeed(double multiplier)
|
||||
{
|
||||
m_gameSpeedMultiplier = multiplier;
|
||||
emit stateUpdated(m_sim->currentTick(),
|
||||
m_sim->buildingBlocksStock(),
|
||||
m_gameSpeedMultiplier);
|
||||
}
|
||||
Reference in New Issue
Block a user