draw building icons in the game world
This commit is contained in:
@@ -9,10 +9,12 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QColor>
|
||||
#include <QCoreApplication>
|
||||
#include <QCursor>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFont>
|
||||
#include <QKeyEvent>
|
||||
#include <QLinearGradient>
|
||||
@@ -21,10 +23,13 @@
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPen>
|
||||
#include <QPixmap>
|
||||
#include <QPolygonF>
|
||||
#include <QRadialGradient>
|
||||
#include <QRegion>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
#include <QSvgRenderer>
|
||||
#include <QTimer>
|
||||
|
||||
#include "AttackBehavior.h"
|
||||
@@ -77,6 +82,52 @@
|
||||
namespace
|
||||
{
|
||||
|
||||
// --- World building icons (REQ-UI-WORLD-ICON) --------------------------------
|
||||
|
||||
// Building types that render with a world icon, and the icon file name for each.
|
||||
// Belts, splitters, and tunnels are intentionally absent so their orientation
|
||||
// stays readable; the two defence stations share one symbol (colored per side by
|
||||
// the fill it is drawn over).
|
||||
struct WorldIconEntry { BuildingType type; const char* file; };
|
||||
const WorldIconEntry kWorldIconFiles[] = {
|
||||
{ BuildingType::Miner, "miner.svg" },
|
||||
{ BuildingType::Smelter, "smelter.svg" },
|
||||
{ BuildingType::Assembler, "assembler.svg" },
|
||||
{ BuildingType::ReprocessingPlant, "reprocessing_plant.svg" },
|
||||
{ BuildingType::Shipyard, "shipyard.svg" },
|
||||
{ BuildingType::SalvageBay, "salvage_bay.svg" },
|
||||
{ BuildingType::Hq, "hq.svg" },
|
||||
{ BuildingType::PlayerDefenceStation, "station.svg" },
|
||||
{ BuildingType::EnemyDefenceStation, "station.svg" },
|
||||
};
|
||||
|
||||
// On-screen size of every world icon, as a multiple of one tile (REQ-UI-WORLD-ICON):
|
||||
// a little over one tile so all buildings' icons read at the same size regardless
|
||||
// of footprint.
|
||||
const qreal kWorldIconTileFactor = 1.25;
|
||||
|
||||
// Produces an icon SVG containing only the glyph (the chip background rect
|
||||
// stripped), with the white glyph stroke recolored to inkHex.
|
||||
QByteArray worldIconSvg(const QByteArray& svg, const QString& inkHex)
|
||||
{
|
||||
QString s = QString::fromUtf8(svg);
|
||||
// Drop the full-canvas chip rect only; some glyphs use their own <rect>
|
||||
// elements (e.g. the miner's drill housing, the hq's base), so match the
|
||||
// 100x100 background rect specifically.
|
||||
static const QRegularExpression backgroundRect(
|
||||
QStringLiteral("<rect\\s+width=\"100\"\\s+height=\"100\"[^>]*>"));
|
||||
s.remove(backgroundRect);
|
||||
s.replace(QStringLiteral("#ffffff"), inkHex);
|
||||
return s.toUtf8();
|
||||
}
|
||||
|
||||
// Perceived luminance test; picks a dark glyph on light fills so it stays legible.
|
||||
bool isLightFill(const QColor& c)
|
||||
{
|
||||
const double lum = (0.299 * c.red() + 0.587 * c.green() + 0.114 * c.blue()) / 255.0;
|
||||
return lum > 0.6;
|
||||
}
|
||||
|
||||
// Keep only the filter entries whose item type is currently unlocked
|
||||
// (REQ-LOCK-UI-BLUEPRINT). An empty result means "accept all".
|
||||
std::vector<ItemType> filterUnlockedItems(const std::vector<ItemType>& filter,
|
||||
@@ -169,6 +220,8 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setMouseTracking(true);
|
||||
|
||||
loadBuildingIcons(configDir);
|
||||
|
||||
m_renderTimer = new QTimer(this);
|
||||
m_renderTimer->setInterval(16);
|
||||
connect(m_renderTimer, &QTimer::timeout, this, &GameWorldView::onFrame);
|
||||
@@ -1209,6 +1262,54 @@ void GameWorldView::drawTiles(QPainter& painter)
|
||||
}
|
||||
}
|
||||
|
||||
void GameWorldView::loadBuildingIcons(const std::string& configDir)
|
||||
{
|
||||
// Icons live beside the config dir, read at runtime like visuals.toml
|
||||
// (REQ-UI-WORLD-ICON), mirroring how MainWindow derives the same path.
|
||||
const QString iconDir = QDir::cleanPath(
|
||||
QString::fromStdString(configDir) + "/../icons/buildings");
|
||||
|
||||
for (const WorldIconEntry& entry : kWorldIconFiles)
|
||||
{
|
||||
QFile file(iconDir + "/" + QString::fromLatin1(entry.file));
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
continue; // A missing icon is not an error; the text glyph is used.
|
||||
}
|
||||
const QByteArray svg = file.readAll();
|
||||
BuildingIconRenderers renderers;
|
||||
renderers.white = std::make_unique<QSvgRenderer>(
|
||||
worldIconSvg(svg, QStringLiteral("#ffffff")));
|
||||
renderers.dark = std::make_unique<QSvgRenderer>(
|
||||
worldIconSvg(svg, QStringLiteral("#1c1c20")));
|
||||
m_buildingIcons[entry.type] = std::move(renderers);
|
||||
}
|
||||
}
|
||||
|
||||
bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type,
|
||||
const QRectF& box, const QColor& fill) const
|
||||
{
|
||||
const std::map<BuildingType, BuildingIconRenderers>::const_iterator it =
|
||||
m_buildingIcons.find(type);
|
||||
if (it == m_buildingIcons.end()) { return false; }
|
||||
|
||||
// Every icon is the same fixed on-screen size, centered on the footprint,
|
||||
// regardless of footprint size (REQ-UI-WORLD-ICON).
|
||||
const qreal side = static_cast<qreal>(getTilePx()) * kWorldIconTileFactor;
|
||||
const QRectF target(box.center().x() - side / 2.0,
|
||||
box.center().y() - side / 2.0, side, side);
|
||||
QSvgRenderer* renderer = isLightFill(fill) ? it->second.dark.get()
|
||||
: it->second.white.get();
|
||||
|
||||
// Render the glyph as vector at the view scale so it stays crisp (a
|
||||
// pre-rasterized pixmap downscaled to tile size looked blurry).
|
||||
const bool wasAntialiasing = painter.testRenderHint(QPainter::Antialiasing);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
renderer->render(&painter, target);
|
||||
painter.setRenderHint(QPainter::Antialiasing, wasAntialiasing);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameWorldView::drawBuildings(QPainter& painter)
|
||||
{
|
||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
||||
@@ -1233,7 +1334,9 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(bboxRect);
|
||||
|
||||
if (!bv.glyph.isEmpty())
|
||||
// Icon glyph over the fill (REQ-UI-WORLD-ICON); falls back to the text
|
||||
// glyph for building types without a world icon (e.g. tunnels).
|
||||
if (!drawBuildingIcon(painter, b.type, bboxRect, bv.fill) && !bv.glyph.isEmpty())
|
||||
{
|
||||
painter.setPen(bv.outline);
|
||||
painter.drawText(bboxRect, Qt::AlignCenter, bv.glyph);
|
||||
@@ -1328,7 +1431,14 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
||||
const QString pctText = QString::number(pct) + "%";
|
||||
|
||||
painter.setPen(bv.outline);
|
||||
if (!bv.glyph.isEmpty())
|
||||
// Identity symbol with the progress percentage below it
|
||||
// (REQ-UI-CONSTRUCTION-PROGRESS): the world icon centered on the
|
||||
// footprint where the type has one, otherwise the text glyph.
|
||||
if (drawBuildingIcon(painter, s.type, bboxRect, bv.fill))
|
||||
{
|
||||
painter.drawText(bboxRect, Qt::AlignHCenter | Qt::AlignBottom, pctText);
|
||||
}
|
||||
else if (!bv.glyph.isEmpty())
|
||||
{
|
||||
const QRectF topHalf(bboxRect.x(), bboxRect.y(),
|
||||
bboxRect.width(), bboxRect.height() * 0.5);
|
||||
@@ -1578,6 +1688,10 @@ void GameWorldView::drawStations(QPainter& painter)
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(bboxRect);
|
||||
|
||||
// Station icon over the fill (REQ-UI-WORLD-ICON); the same symbol is
|
||||
// colored blue/red by the player/enemy fill it is drawn over.
|
||||
drawBuildingIcon(painter, visType, bboxRect, bv.fill);
|
||||
|
||||
if (isEntitySelected(e))
|
||||
{
|
||||
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
|
||||
@@ -2082,7 +2196,9 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(bboxRect);
|
||||
|
||||
if (!bv.glyph.isEmpty())
|
||||
// Icon glyph over the ghost fill (REQ-UI-WORLD-ICON); an invalid ghost's fill
|
||||
// is the red invalid color, which auto-contrasts to a white icon.
|
||||
if (!drawBuildingIcon(painter, type, bboxRect, fillColor) && !bv.glyph.isEmpty())
|
||||
{
|
||||
painter.setPen(lineColor);
|
||||
painter.drawText(bboxRect, Qt::AlignCenter, bv.glyph);
|
||||
|
||||
Reference in New Issue
Block a user