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:
@@ -473,7 +473,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
- **Narrow contest zone:** should the two ramp bands overlap (a contest zone narrower than the band width), each ramp is clamped at the contest-zone center so the bands do not cross; the fast plateau then reduces to a single point at the center and the peak speed there may be below the fast speed.
|
||||
|
||||
Because the contest-zone boundaries shift as the scrollable area grows with each push (REQ-GW-PUSH-EXPAND, REQ-GW-SCROLL-LIMIT), the ramp bands are recomputed from the current contest-zone boundaries. This is a presentation-only concern and does not affect the simulation, consistent with REQ-UI-NO-ZOOM.
|
||||
- REQ-UI-WORLD-ICON: In the game world, a building is drawn with an icon's glyph symbol centered on its footprint, in place of the letter identity glyph. The icon is an SVG loaded from `data/icons/buildings/`; only the icon's white glyph is drawn in the world — its colored chip background is omitted, because the footprint is already filled with the building's `visuals.toml` fill color. This applies to the production buildings (Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, Salvage Bay), the HQ, and the player and enemy defence stations, wherever the identity label appears: operational buildings, construction sites (REQ-UI-CONSTRUCTION-PROGRESS), and the builder-mode and blueprint-placement ghosts. **Belts, splitters, and tunnels are excluded** — they keep their existing tile rendering so their orientation and flow stay readable (a centered icon would obscure direction). Their build-menu buttons still use icons (REQ-UI-BUILD-ICON); in particular the shared Tunnel button's `tunnel_entry.svg` is a build-button icon only, not a world icon. The directional output-port glyphs (REQ-UI-PORT-GLYPH, REQ-UI-PORT-TARGET-GLYPH) are a separate indicator and are unaffected. A building or station with no icon file falls back to its `visuals.toml` text glyph; a type with neither icon nor glyph shows no identity label. A missing icon is not an error, consistent with REQ-UI-BUILD-ICON.
|
||||
- REQ-UI-WORLD-ICON: In the game world, a building is drawn with an icon's glyph symbol centered on its footprint, in place of the letter identity glyph. The icon is an SVG loaded from `data/icons/buildings/`; only the icon's glyph is drawn in the world — in a contrasting ink (white over dark fills, dark over light fills) so it stays legible — and its colored chip background is omitted, because the footprint is already filled with the building's `visuals.toml` fill color. This applies to the production buildings (Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, Salvage Bay), the HQ, and the player and enemy defence stations, wherever the identity label appears: operational buildings, construction sites (REQ-UI-CONSTRUCTION-PROGRESS), and the builder-mode and blueprint-placement ghosts. **Belts, splitters, and tunnels are excluded** — they keep their existing tile rendering so their orientation and flow stay readable (a centered icon would obscure direction). Their build-menu buttons still use icons (REQ-UI-BUILD-ICON); in particular the shared Tunnel button's `tunnel_entry.svg` is a build-button icon only, not a world icon. The directional output-port glyphs (REQ-UI-PORT-GLYPH, REQ-UI-PORT-TARGET-GLYPH) are a separate indicator and are unaffected. A building or station with no icon file falls back to its `visuals.toml` text glyph; a type with neither icon nor glyph shows no identity label. A missing icon is not an error, consistent with REQ-UI-BUILD-ICON.
|
||||
- REQ-UI-CONSTRUCTION-PROGRESS: Construction sites display the building's identity symbol centered on the footprint (same as an operational building) — its icon glyph, or the text glyph as a fallback (REQ-UI-WORLD-ICON). Below the symbol — or centered on the footprint if the building has neither an icon nor a glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes.
|
||||
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
|
||||
- REQ-UI-PORT-TARGET-GLYPH: While in builder mode (REQ-BLD-BUILDER-MODE), the builder-mode ghost additionally shows, for each of the building's output ports, a directional glyph drawn centered in the port's **target cell** — the cell immediately outside the footprint that the port pushes into, i.e. the cell the surface-mask output-port indicator occupies (see Surface Mask Format). As in REQ-UI-PORT-GLYPH the glyph is a `>` rotated to face the port's exit direction (`>` East, `^` North, `<` West, `v` South), previewing where the port's output will go before placement. This is in addition to the on-tile port glyph of REQ-UI-PORT-GLYPH, and — unlike that indicator — is shown only for the builder-mode ghost, not for operational buildings, construction sites, or the blueprint-placement ghost (REQ-UI-BLUEPRINT-PLACE). A building with multiple output ports (e.g. a splitter) shows one target-cell glyph per port. The target-cell glyph is drawn larger than the on-tile port glyph so it stands out as the flow-direction preview. Exceptions: the Tunnel Entry shows no target-cell glyph, because it receives items (which may arrive from any of its non-mouth edges, REQ-BLD-TUNNEL-ENTRY) rather than emitting into a single adjacent cell; the Shipyard shows none either, because its output port is a ship-spawn point (REQ-SHP-SPAWN-PLAYER) rather than a belt-item output (REQ-MAT-OUTPUT-EMERGE).
|
||||
|
||||
@@ -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