draw building icons in the game world

This commit is contained in:
2026-07-22 22:49:03 +02:00
parent 7c1455b8a0
commit 60d6767d93
6 changed files with 162 additions and 6 deletions

View File

@@ -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);

View File

@@ -1,6 +1,7 @@
#pragma once
#include <map>
#include <memory>
#include <optional>
#include <random>
#include <set>
@@ -8,6 +9,7 @@
#include <utility>
#include <vector>
#include <QColor>
#include <QElapsedTimer>
#include <QOpenGLWidget>
#include <QPoint>
@@ -52,6 +54,7 @@ struct ParsedReplay;
class ReplayPlayer;
class Simulation;
class QPainter;
class QSvgRenderer;
struct QPointCompare
{
@@ -187,6 +190,19 @@ private:
QPoint anchorTile, Rotation rotation, bool valid,
bool showPortTargetGlyphs);
// Loads the per-building world icons (REQ-UI-WORLD-ICON) from
// <configDir>/../icons/buildings once at construction. Only the building
// types with a world icon are loaded (production buildings, HQ, stations);
// belts, splitters, and tunnels are deliberately excluded so their
// orientation stays readable. The SVG's chip background is stripped; the
// glyph is pre-rendered in both white and dark ink for auto-contrast.
void loadBuildingIcons(const std::string& configDir);
// Draws a building's world icon glyph centered in box, choosing the white or
// dark pre-rendered variant by fill luminance so it stays legible. Returns
// false if the type has no world icon (caller falls back to the text glyph).
bool drawBuildingIcon(QPainter& painter, BuildingType type,
const QRectF& box, const QColor& fill) const;
void placeBlueprintAtTile(QPoint center);
std::optional<QVector2D> entityPosition(entt::entity entity) const;
@@ -269,6 +285,17 @@ private:
const GameConfig* m_config;
const VisualsConfig* m_visuals;
// World icon glyph renderers per building type (REQ-UI-WORLD-ICON), in a
// white and a dark variant so drawBuildingIcon can auto-contrast against the
// building's fill. Rendered as vector at the view scale each draw so they
// stay crisp. Populated once by loadBuildingIcons().
struct BuildingIconRenderers
{
std::unique_ptr<QSvgRenderer> white;
std::unique_ptr<QSvgRenderer> dark;
};
std::map<BuildingType, BuildingIconRenderers> m_buildingIcons;
// Funnels all player input into the single Simulation::apply chokepoint.
CommandManager m_commandManager;
// A Reset command was enqueued; reset the view after the next drain applies it.