Add building status light

This commit is contained in:
2026-07-20 22:19:58 +02:00
parent c7ecce6ac4
commit 1cbc695bc5
8 changed files with 385 additions and 57 deletions

View File

@@ -360,3 +360,17 @@ modal_dim = "#00000099" # semi-transparent black dim behind modal dialogs
bg = "#000000cc"
fg = "#ffffff"
font_size = 14
# -----------------------------------------------------------------------------
# Building status light (REQ-UI-STATUS-LIGHT)
#
# Fill color per production state, drawn as a small circle in the building's
# upper-right corner, plus the constant outline color.
# -----------------------------------------------------------------------------
[status_light]
grey = "#808080" # no recipe/schematic selected
green = "#33cc33" # producing (Salvage Bay: holding scrap)
red = "#cc3333" # idle, input missing (Salvage Bay: empty)
yellow = "#e6c619" # idle, output buffer full
outline = "#000000"

View File

@@ -10,7 +10,7 @@ Config files use the TOML format. The following config files drive game paramete
- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as plain values, required build materials, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), an optional `unlock_requires` prerequisite list (REQ-LOCK-PREREQ), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
- **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), the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the module schematic already unlocked), an optional `unlock_requires` prerequisite list (REQ-LOCK-PREREQ), and an optional capability section and/or stat modifier formulas. 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).
- **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. 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 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.
@@ -448,6 +448,15 @@ The screen is divided into two columns: a main column (75% width) containing the
- 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-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.
- For the five production buildings (Miner, Smelter, Assembler, Reprocessing Plant, Shipyard), the fill color is determined by evaluating, in order:
- **Grey** — no recipe or schematic is selected. This applies only to buildings with a player-facing selection (Miner, Assembler, Shipyard); the Smelter and Reprocessing Plant always run an implicit recipe (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) and are never grey.
- **Green** — the building is currently producing: a production cycle is active (REQ-MAT-CYCLE; for the Shipyard, an in-progress production cycle per REQ-BLD-SHIPYARD).
- **Red** — the building is idle because a required input is missing from its input buffers, so it cannot start a cycle. Missing input takes precedence over a full output buffer: if any required input is missing the light is red even when the output buffer is also full.
- **Yellow** — the building is idle with all required inputs present but its output buffer full, so no new cycle can start (REQ-MAT-OUTPUT-BUFFER, REQ-MAT-CYCLE).
- A configured building that is momentarily idle yet blocked by neither condition (all inputs present and the output buffer has room — a transient state that resolves into a started cycle on the same or the next tick per REQ-MAT-CYCLE) shows green.
- The Salvage Bay has no recipe and no production cycle (REQ-BLD-SALVAGE-BAY); its status light uses only two states: **green** while its output buffer holds at least one unit of scrap, and **red** while its output buffer is empty. The Salvage Bay's status light is never grey or yellow.
- The four fill colors (grey, green, red, yellow) and the outline color are read from `visuals.toml [status_light]`, consistent with the other rendering-only colors. The status light is presentation-only and has no effect on the simulation.
- REQ-UI-HP-BARS: All entities with HP — the HQ, player and enemy defence stations, and player and enemy ships — render an HP bar below them. The bar is always visible regardless of current HP. The bar's filled portion represents the fraction of current HP to maximum HP.
- REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out.
- REQ-UI-HOTKEYS: Global keyboard shortcuts:

View File

@@ -951,43 +951,13 @@ void BuildingSystem::tickProduction(Tick currentTick)
// recipe of their type in config order, running the first whose inputs
// are satisfied (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). Other buildings
// try only their selected recipe.
std::vector<const RecipeDef*> candidates;
if (autoRecipe)
{
for (const RecipeDef& r : m_config.recipes.recipes)
{
if (r.building == building.type && !r.inputs.empty())
{
candidates.push_back(&r);
}
}
}
else
{
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
if (recipe)
{
candidates.push_back(recipe);
}
}
const std::vector<const RecipeDef*> candidates =
gatherCandidateRecipes(building);
for (const RecipeDef* recipe : candidates)
{
// 1. All required inputs present?
bool inputsOk = true;
for (const RecipeIngredient& ing : recipe->inputs)
{
const ItemType type{ing.item};
const std::map<ItemType, int>::const_iterator it =
building.inputBuffer.counts.find(type);
const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
if (have < ing.amount)
{
inputsOk = false;
break;
}
}
if (!inputsOk)
if (!recipeInputsAvailable(building, *recipe))
{
continue;
}
@@ -1082,26 +1052,8 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
}
// Build combined materials list (base + modules).
std::map<std::string, int> requiredMaterials;
for (const RecipeIngredient& ing : shipDef->schematic.materials)
{
requiredMaterials[ing.item] += ing.amount;
}
if (building.shipLayout.has_value())
{
for (const PlacedModule& pm : building.shipLayout->placedModules)
{
const ModuleDef* modDef = findModuleDef(pm.moduleId);
if (!modDef)
{
continue;
}
for (const RecipeIngredient& ing : modDef->materials)
{
requiredMaterials[ing.item] += ing.amount;
}
}
}
const std::map<std::string, int> requiredMaterials =
computeShipyardRequiredMaterials(building);
// Idle: check if all combined materials are available.
bool inputsOk = true;
@@ -1328,6 +1280,147 @@ int BuildingSystem::getActiveProductionBuildingCount() const
return count;
}
std::vector<const RecipeDef*>
BuildingSystem::gatherCandidateRecipes(const Building& b) const
{
std::vector<const RecipeDef*> candidates;
if (isAutoRecipeBuildingType(b.type))
{
for (const RecipeDef& r : m_config.recipes.recipes)
{
if (r.building == b.type && !r.inputs.empty())
{
candidates.push_back(&r);
}
}
}
else
{
const RecipeDef* recipe = findRecipe(b.recipeId, b.type);
if (recipe)
{
candidates.push_back(recipe);
}
}
return candidates;
}
bool BuildingSystem::recipeInputsAvailable(const Building& b,
const RecipeDef& recipe) const
{
for (const RecipeIngredient& ing : recipe.inputs)
{
const std::map<ItemType, int>::const_iterator it =
b.inputBuffer.counts.find(ItemType{ing.item});
const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
if (have < ing.amount)
{
return false;
}
}
return true;
}
std::map<std::string, int>
BuildingSystem::computeShipyardRequiredMaterials(const Building& b) const
{
std::map<std::string, int> requiredMaterials;
const ShipDef* shipDef = findShipDef(b.recipeId);
if (!shipDef)
{
return requiredMaterials;
}
for (const RecipeIngredient& ing : shipDef->schematic.materials)
{
requiredMaterials[ing.item] += ing.amount;
}
if (b.shipLayout.has_value())
{
for (const PlacedModule& pm : b.shipLayout->placedModules)
{
const ModuleDef* modDef = findModuleDef(pm.moduleId);
if (!modDef)
{
continue;
}
for (const RecipeIngredient& ing : modDef->materials)
{
requiredMaterials[ing.item] += ing.amount;
}
}
}
return requiredMaterials;
}
bool BuildingSystem::hasInputsToStart(const Building& b) const
{
if (b.type == BuildingType::Shipyard)
{
const std::map<std::string, int> required =
computeShipyardRequiredMaterials(b);
for (const std::pair<const std::string, int>& req : required)
{
const std::map<ItemType, int>::const_iterator it =
b.inputBuffer.counts.find(ItemType{req.first});
const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
if (have < req.second)
{
return false;
}
}
return true;
}
// Recipe buildings: startable if any candidate recipe's inputs are satisfied.
// A Miner recipe has no inputs, so an idle Miner is always startable and its
// only idle reason is a full output buffer.
for (const RecipeDef* recipe : gatherCandidateRecipes(b))
{
if (recipeInputsAvailable(b, *recipe))
{
return true;
}
}
return false;
}
std::optional<ProductionStatus>
BuildingSystem::getProductionStatus(const Building& building) const
{
// Salvage Bay has no recipe or production cycle (REQ-BLD-SALVAGE-BAY): it is
// "producing" while it holds scrap to push out, and starved when empty.
if (building.type == BuildingType::SalvageBay)
{
return building.getOutputItemCount() >= 1 ? ProductionStatus::Producing
: ProductionStatus::Starved;
}
// Only the five recipe/cycle production types show a status light besides the
// Salvage Bay; belts, splitters, tunnels, HQ, and stations show none.
if (!isProductionBuildingType(building.type))
{
return std::nullopt;
}
// Grey only applies to player-configured types; auto-recipe buildings
// (Smelter, Reprocessing Plant) always run an implicit recipe.
if (!isAutoRecipeBuildingType(building.type) && building.recipeId.empty())
{
return ProductionStatus::Unconfigured;
}
if (building.production.has_value())
{
return ProductionStatus::Producing;
}
// Idle: missing inputs (red) take precedence over a full output buffer
// (yellow). If inputs are present yet the building is idle, the only remaining
// reason it could not start a cycle is a full output buffer (REQ-MAT-CYCLE).
return hasInputsToStart(building) ? ProductionStatus::Blocked
: ProductionStatus::Starved;
}
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::getAllBeltTiles() const
{
std::vector<BeltTileInfo> result;

View File

@@ -26,6 +26,18 @@
class Hasher;
// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT).
// The simulation owns the classification so it stays in sync with the
// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill
// color.
enum class ProductionStatus
{
Unconfigured, // no recipe/schematic selected (grey)
Producing, // a production cycle is active (green)
Starved, // idle: a required input is missing / Salvage Bay empty (red)
Blocked, // idle: output buffer full, inputs otherwise present (yellow)
};
// Manages building placement, construction queuing, and the per-tick
// production loop (belt→building pull, production, building→belt push).
// All types including Belt and Splitter are stored as Building instances;
@@ -122,6 +134,12 @@ public:
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
// that currently has an active production cycle.
int getActiveProductionBuildingCount() const;
// Production state for the UI status light (REQ-UI-STATUS-LIGHT). Returns
// nullopt for building types that show no light (belts, splitters, tunnels,
// HQ, defence stations). The Salvage Bay is a two-state special case:
// Producing while its output buffer holds scrap, Starved when empty.
std::optional<ProductionStatus> getProductionStatus(const Building& building) const;
std::vector<BeltTileInfo> getAllBeltTiles() const;
bool isTileOccupied(QPoint tile) const;
@@ -200,6 +218,22 @@ private:
const Port& outputPort,
const Item& item);
// Candidate recipes an idle building would try this tick: an auto-recipe
// building (Smelter, Reprocessing Plant) offers every recipe of its type with
// inputs; other buildings offer only their selected recipe. Shared by
// tickProduction and the status classifier (REQ-MAT-CYCLE, REQ-UI-STATUS-LIGHT).
std::vector<const RecipeDef*> gatherCandidateRecipes(const Building& b) const;
// True if every input of `recipe` is present in `b`'s input buffers in the
// required per-cycle amount (REQ-MAT-CYCLE input check).
bool recipeInputsAvailable(const Building& b,
const RecipeDef& recipe) const;
// Combined base + module materials a shipyard needs per ship (REQ-BLD-SHIPYARD).
std::map<std::string, int> computeShipyardRequiredMaterials(const Building& b) const;
// True if the building currently has all inputs/materials to start a cycle
// (ignoring output-buffer space); drives the Starved/Blocked distinction of
// the status light (REQ-UI-STATUS-LIGHT).
bool hasInputsToStart(const Building& b) const;
const BuildingDef* findBuildingDef(BuildingType type) const;
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
const ShipDef* findShipDef(const std::string& id) const;

View File

@@ -1308,3 +1308,116 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
REQUIRE(builtInfo->filterA == filterA);
REQUIRE(builtInfo->filterB.empty());
}
// ---------------------------------------------------------------------------
// Production status classifier (REQ-UI-STATUS-LIGHT)
// ---------------------------------------------------------------------------
TEST_CASE("BuildingSystem: getProductionStatus classifies production state", "[building]")
{
PlacementFixture f;
// Pick representative config ids so the test survives content edits.
std::string minerRecipeId;
for (const RecipeDef& r : f.cfg.recipes.recipes)
{
if (r.building == BuildingType::Miner) { minerRecipeId = r.id; break; }
}
REQUIRE_FALSE(minerRecipeId.empty());
const RecipeDef* assemblerRecipe = nullptr;
for (const RecipeDef& r : f.cfg.recipes.recipes)
{
if (r.building == BuildingType::Assembler && !r.inputs.empty())
{
assemblerRecipe = &r;
break;
}
}
REQUIRE(assemblerRecipe != nullptr);
std::string shipId;
for (const ShipDef& s : f.cfg.ships.ships)
{
if (!s.schematic.materials.empty()) { shipId = s.id; break; }
}
REQUIRE_FALSE(shipId.empty());
const auto statusOf = [&f](const Building& b) { return f.bs.getProductionStatus(b); };
SECTION("non-production buildings show no status light")
{
Building belt; belt.type = BuildingType::Belt;
Building hq; hq.type = BuildingType::Hq;
REQUIRE_FALSE(statusOf(belt).has_value());
REQUIRE_FALSE(statusOf(hq).has_value());
}
SECTION("Miner: unconfigured, producing, output-blocked")
{
Building miner; miner.type = BuildingType::Miner;
REQUIRE(statusOf(miner) == ProductionStatus::Unconfigured); // no recipe -> grey
miner.recipeId = minerRecipeId;
miner.production = Production{};
REQUIRE(statusOf(miner) == ProductionStatus::Producing); // active cycle -> green
// A miner has no inputs, so its only idle reason is a full output buffer.
miner.production = std::nullopt;
miner.outputBuffer.capacity = 2;
miner.outputBuffer.items = { makeItem("iron_ore"), makeItem("iron_ore") };
REQUIRE(statusOf(miner) == ProductionStatus::Blocked); // -> yellow
}
SECTION("Assembler: starved vs blocked, input-missing takes precedence")
{
Building assembler; assembler.type = BuildingType::Assembler;
assembler.recipeId = assemblerRecipe->id;
// Idle with inputs missing -> red.
REQUIRE(statusOf(assembler) == ProductionStatus::Starved);
// Inputs present but idle -> the only remaining reason is a full output
// buffer -> yellow.
for (const RecipeIngredient& ing : assemblerRecipe->inputs)
{
assembler.inputBuffer.counts[ItemType{ing.item}] = ing.amount;
}
REQUIRE(statusOf(assembler) == ProductionStatus::Blocked);
// Inputs missing AND output full -> red wins over yellow.
assembler.inputBuffer.counts.clear();
assembler.outputBuffer.capacity = 2;
assembler.outputBuffer.items = { makeItem("x"), makeItem("x") };
REQUIRE(statusOf(assembler) == ProductionStatus::Starved);
}
SECTION("Smelter (auto-recipe) is never grey")
{
Building smelter; smelter.type = BuildingType::Smelter;
// No player-selectable recipe and empty inputs -> red, not grey.
REQUIRE(statusOf(smelter) == ProductionStatus::Starved);
}
SECTION("Shipyard: unconfigured, then starved without materials, then producing")
{
Building yard; yard.type = BuildingType::Shipyard;
REQUIRE(statusOf(yard) == ProductionStatus::Unconfigured); // no schematic -> grey
yard.recipeId = shipId;
REQUIRE(statusOf(yard) == ProductionStatus::Starved); // no materials -> red
yard.production = Production{};
REQUIRE(statusOf(yard) == ProductionStatus::Producing); // active cycle -> green
}
SECTION("Salvage Bay: red when empty, green when holding scrap")
{
Building bay; bay.type = BuildingType::SalvageBay;
bay.outputBuffer.capacity = 20;
REQUIRE(statusOf(bay) == ProductionStatus::Starved); // empty -> red
bay.outputBuffer.items = { makeItem("scrap") };
REQUIRE(statusOf(bay) == ProductionStatus::Producing); // holding scrap -> green
}
}

View File

@@ -124,6 +124,20 @@ QPoint portBodyTile(QPoint portTile, Rotation direction)
return portTile;
}
// Fill color for a building's status light per its production state
// (REQ-UI-STATUS-LIGHT).
QColor statusLightFill(ProductionStatus status, const StatusLightVisuals& sl)
{
switch (status)
{
case ProductionStatus::Unconfigured: return sl.grey;
case ProductionStatus::Producing: return sl.green;
case ProductionStatus::Starved: return sl.red;
case ProductionStatus::Blocked: return sl.yellow;
}
return sl.grey;
}
} // namespace
@@ -1039,6 +1053,35 @@ void GameWorldView::drawBuildings(QPainter& painter)
port.direction, bv.outline, /*centered*/ false);
}
// Status light: a small circle in the building's upper-right corner
// (in default/East orientation), anchored to that footprint corner and
// rotating with the building (REQ-UI-STATUS-LIGHT). All status-light
// building footprints are rectangular, so a corner of the axis-aligned
// bounding box is the true corner.
if (const std::optional<ProductionStatus> status =
m_sim->getBuildings().getProductionStatus(b))
{
const float px = getTilePx();
const float r = px * 0.18f;
const float inset = r + px * 0.12f;
// Default orientation (East) puts the light at the top-right corner;
// clockwise rotation carries it around the footprint.
QPointF center(bboxRect.right() - inset, bboxRect.top() + inset);
switch (b.rotation)
{
case Rotation::East: break;
case Rotation::South: center = QPointF(bboxRect.right() - inset,
bboxRect.bottom() - inset); break;
case Rotation::West: center = QPointF(bboxRect.left() + inset,
bboxRect.bottom() - inset); break;
case Rotation::North: center = QPointF(bboxRect.left() + inset,
bboxRect.top() + inset); break;
}
painter.setBrush(statusLightFill(*status, m_visuals->statusLight));
painter.setPen(QPen(m_visuals->statusLight.outline, 1));
painter.drawEllipse(center, r, r);
}
// HP bar below the HQ footprint; the HQ's HP lives on its proxy entity.
if (b.type == BuildingType::Hq)
{

View File

@@ -60,6 +60,17 @@ struct ToastVisuals
int fontSize;
};
// Fill colors for the building status light (REQ-UI-STATUS-LIGHT), plus its
// constant outline color.
struct StatusLightVisuals
{
QColor grey; // no recipe/schematic selected
QColor green; // producing
QColor red; // idle, input missing (or Salvage Bay empty)
QColor yellow; // idle, output buffer full
QColor outline;
};
struct VisualsConfig
{
TileVisuals asteroid;
@@ -69,7 +80,8 @@ struct VisualsConfig
std::map<std::string, ItemVisuals> items;
std::map<std::string, ShipVisuals> ships;
BeamVisuals beams;
OverlayVisuals overlays;
ToastVisuals toast;
BeamVisuals beams;
OverlayVisuals overlays;
ToastVisuals toast;
StatusLightVisuals statusLight;
};

View File

@@ -237,5 +237,15 @@ VisualsConfig VisualsLoader::load(const std::string& path)
cfg.toast.fontSize = requireInt(t, "font_size", "toast");
}
// Status light (REQ-UI-STATUS-LIGHT)
{
toml::table& sl = requireSubtable(tbl, "status_light", "root");
cfg.statusLight.grey = parseColor(requireString(sl, "grey", "status_light"), "status_light.grey");
cfg.statusLight.green = parseColor(requireString(sl, "green", "status_light"), "status_light.green");
cfg.statusLight.red = parseColor(requireString(sl, "red", "status_light"), "status_light.red");
cfg.statusLight.yellow = parseColor(requireString(sl, "yellow", "status_light"), "status_light.yellow");
cfg.statusLight.outline = parseColor(requireString(sl, "outline", "status_light"), "status_light.outline");
}
return cfg;
}