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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user