From 60d6767d93483ddd9a7fa9875bc3fe947746ac91 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Wed, 22 Jul 2026 22:49:03 +0200 Subject: [PATCH] draw building icons in the game world --- bin/app/data/icons/buildings/hq.svg | 6 + bin/app/data/icons/buildings/station.svg | 6 + bin/app/data/icons/buildings/tunnel_entry.svg | 2 +- docs/requirements.md | 5 +- src/ui/GameWorldView.cpp | 122 +++++++++++++++++- src/ui/GameWorldView.h | 27 ++++ 6 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 bin/app/data/icons/buildings/hq.svg create mode 100644 bin/app/data/icons/buildings/station.svg diff --git a/bin/app/data/icons/buildings/hq.svg b/bin/app/data/icons/buildings/hq.svg new file mode 100644 index 0000000..3629202 --- /dev/null +++ b/bin/app/data/icons/buildings/hq.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/bin/app/data/icons/buildings/station.svg b/bin/app/data/icons/buildings/station.svg new file mode 100644 index 0000000..a9f02d2 --- /dev/null +++ b/bin/app/data/icons/buildings/station.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/bin/app/data/icons/buildings/tunnel_entry.svg b/bin/app/data/icons/buildings/tunnel_entry.svg index b31390d..663f2fa 100644 --- a/bin/app/data/icons/buildings/tunnel_entry.svg +++ b/bin/app/data/icons/buildings/tunnel_entry.svg @@ -1,6 +1,6 @@ - + diff --git a/docs/requirements.md b/docs/requirements.md index ee2bacd..641fbb5 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -11,7 +11,7 @@ Config files use the TOML format. The following config files drive game paramete - **modules.toml** — per module type: id, surface mask, materials list, production time, fill color, glyph, an optional tooltip description string (shown as the module selection button's hover tooltip, REQ-MOD-UI-MODULE-TOOLTIP; omitted when unset), and an optional capability section and/or stat modifier formulas. Whether a module schematic is available from game start or must be unlocked during play is defined in **unlocks.toml** (REQ-LOCK-EXPLICIT), not here. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC). - **unlocks.toml** — unlock groups: each `[[unlock]]` entry names a group of ship schematics, module schematics, building types, and/or assembler recipes that are awarded together from a single defence station drop (see Unlock Group Format, REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP). Anything not granted by any unlock group is available from game start. - **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level. -- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; a distinct beam color per tool type (weapon, repair, salvage) and beam width; overlay and toast colors; and building status light colors (grey, green, red, and yellow fills plus the outline color, REQ-UI-STATUS-LIGHT). Loaded by the UI at startup; the simulation does not read it. +- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs (identity labels; used in the world for building types not covered by an icon and as the fallback when an icon file is missing — REQ-UI-WORLD-ICON) for every building type, item type, ship schematic, and station type; a distinct beam color per tool type (weapon, repair, salvage) and beam width; overlay and toast colors; and building status light colors (grey, green, red, and yellow fills plus the outline color, REQ-UI-STATUS-LIGHT). Loaded by the UI at startup; the simulation does not read it. - **ship_layouts.toml** — named layout blueprints per ship type; written and read by the application to persist the layout blueprint panel (REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). Not a game parameter file; the simulation does not read it. - REQ-CFG-RELOAD: When the player triggers a Restart (REQ-UI-GAME-MENU), all config files are reloaded from disk before the simulation is reset to its initial state. Formula strings are recompiled at that point. This allows config edits made while the application is running to take effect without a full application restart. @@ -473,7 +473,8 @@ 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-CONSTRUCTION-PROGRESS: Construction sites display the building's glyph centered on the footprint (same as an operational building). Below the glyph — or centered on the footprint if the building has no glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes. +- 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). - REQ-UI-STATUS-LIGHT: Every operational production building — Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, and Salvage Bay — renders a small **status light**: a filled circle with a black outline drawn in the building's upper-right corner, letting the player read a building's production state without selecting it. The light is anchored to the footprint corner that is the upper-right corner in the building's default orientation and rotates with the building — like the output-port glyph (REQ-UI-PORT-GLYPH) — so it stays on the same physical corner of the building as it is rotated. The status light is rendered only for operational buildings; construction sites (which instead show construction progress, REQ-UI-CONSTRUCTION-PROGRESS) and the builder-mode ghost do not render it. Buildings that are not production buildings — belts, splitters, tunnel entries/exits, and the HQ — have no status light. The black outline is constant; the fill color reflects the building's current production state. diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 3f6df6d..b63fafe 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -9,10 +9,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -21,10 +23,13 @@ #include #include #include +#include #include #include #include +#include #include +#include #include #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 + // elements (e.g. the miner's drill housing, the hq's base), so match the + // 100x100 background rect specifically. + static const QRegularExpression backgroundRect( + QStringLiteral("]*>")); + 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 filterUnlockedItems(const std::vector& 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( + worldIconSvg(svg, QStringLiteral("#ffffff"))); + renderers.dark = std::make_unique( + 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::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(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); diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index 9d3d400..cf7d4a0 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -8,6 +9,7 @@ #include #include +#include #include #include #include @@ -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 + // /../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 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 white; + std::unique_ptr dark; + }; + std::map 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.