Render building icons in the game world
Draw each building's icon glyph over its footprint fill in place of the text identity glyph (REQ-UI-WORLD-ICON): production buildings, HQ, and defence stations. GameWorldView pre-renders each type's icon (chip background stripped) in a white and a dark variant and picks the contrasting one by fill luminance; belts, splitters, and tunnels are excluded and keep their tile/text rendering. Wired into operational buildings, construction sites (icon + progress), stations (previously letterless), and the builder/blueprint ghost. Update the requirement wording from "white glyph" to a contrasting ink. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
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,54 @@
|
||||
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" },
|
||||
};
|
||||
|
||||
// Renders an icon SVG's glyph only (the chip background rect stripped) into a
|
||||
// transparent pixmap, recoloring the white glyph stroke to inkHex.
|
||||
QPixmap renderWorldIcon(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);
|
||||
|
||||
const int px = 192;
|
||||
QPixmap pixmap(px, px);
|
||||
pixmap.fill(Qt::transparent);
|
||||
QSvgRenderer renderer(s.toUtf8());
|
||||
QPainter painter(&pixmap);
|
||||
renderer.render(&painter);
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
// 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 +222,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 +1264,47 @@ 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();
|
||||
BuildingIconPixmaps pixmaps;
|
||||
pixmaps.white = renderWorldIcon(svg, QStringLiteral("#ffffff"));
|
||||
pixmaps.dark = renderWorldIcon(svg, QStringLiteral("#1c1c20"));
|
||||
m_buildingIcons[entry.type] = pixmaps;
|
||||
}
|
||||
}
|
||||
|
||||
bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type,
|
||||
const QRectF& box, const QColor& fill) const
|
||||
{
|
||||
const std::map<BuildingType, BuildingIconPixmaps>::const_iterator it =
|
||||
m_buildingIcons.find(type);
|
||||
if (it == m_buildingIcons.end()) { return false; }
|
||||
|
||||
// Centered square keeps the glyph undistorted on non-square footprints; the
|
||||
// icon's own 13% margin supplies padding.
|
||||
const qreal side = std::min(box.width(), box.height());
|
||||
const QRectF target(box.center().x() - side / 2.0,
|
||||
box.center().y() - side / 2.0, side, side);
|
||||
const QPixmap& pixmap = isLightFill(fill) ? it->second.dark : it->second.white;
|
||||
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
painter.drawPixmap(target, pixmap, pixmap.rect());
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameWorldView::drawBuildings(QPainter& painter)
|
||||
{
|
||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
||||
@@ -1233,7 +1329,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,13 +1426,19 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
||||
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);
|
||||
// Identity symbol above, progress percentage below (REQ-UI-CONSTRUCTION-PROGRESS):
|
||||
// the world icon where the type has one, otherwise the text glyph.
|
||||
if (drawBuildingIcon(painter, s.type, topHalf, bv.fill))
|
||||
{
|
||||
painter.drawText(botHalf, Qt::AlignCenter, pctText);
|
||||
}
|
||||
else 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);
|
||||
}
|
||||
@@ -1578,6 +1682,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 +2190,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);
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QColor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QPixmap>
|
||||
#include <QPoint>
|
||||
#include <QRectF>
|
||||
#include <QTimer>
|
||||
@@ -187,6 +189,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 +284,12 @@ private:
|
||||
const GameConfig* m_config;
|
||||
const VisualsConfig* m_visuals;
|
||||
|
||||
// Pre-rendered world icon glyphs per building type (REQ-UI-WORLD-ICON), in a
|
||||
// white and a dark variant so drawBuildingIcon can auto-contrast against the
|
||||
// building's fill. Populated once by loadBuildingIcons().
|
||||
struct BuildingIconPixmaps { QPixmap white; QPixmap dark; };
|
||||
std::map<BuildingType, BuildingIconPixmaps> 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.
|
||||
|
||||
Reference in New Issue
Block a user