Implement building status light (REQ-UI-STATUS-LIGHT)
Add a per-building status light: a small circle drawn in the operational building's upper-right corner (rotating with the building) so production state is readable without selecting it. - Sim: BuildingSystem::getProductionStatus classifies each building into Unconfigured/Producing/Starved/Blocked (nullopt for non-production types); Salvage Bay is a two-state special case (Producing with scrap, Starved when empty). Extracted shared predicates (gatherCandidateRecipes, recipeInputsAvailable, computeShipyardRequiredMaterials, hasInputsToStart) so tickProduction / tickShipyardProduction and the classifier stay in sync. - UI: GameWorldView draws the circle, mapping status -> color from a new visuals.toml [status_light] section (grey/green/red/yellow + outline). - Tests: getProductionStatus cases for every state, including red-over-yellow precedence and the auto-recipe/salvage-bay specials. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -360,3 +360,17 @@ modal_dim = "#00000099" # semi-transparent black dim behind modal dialogs
|
|||||||
bg = "#000000cc"
|
bg = "#000000cc"
|
||||||
fg = "#ffffff"
|
fg = "#ffffff"
|
||||||
font_size = 14
|
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"
|
||||||
|
|||||||
@@ -951,43 +951,13 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
|||||||
// recipe of their type in config order, running the first whose inputs
|
// recipe of their type in config order, running the first whose inputs
|
||||||
// are satisfied (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). Other buildings
|
// are satisfied (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). Other buildings
|
||||||
// try only their selected recipe.
|
// try only their selected recipe.
|
||||||
std::vector<const RecipeDef*> candidates;
|
const std::vector<const RecipeDef*> candidates =
|
||||||
if (autoRecipe)
|
gatherCandidateRecipes(building);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const RecipeDef* recipe : candidates)
|
for (const RecipeDef* recipe : candidates)
|
||||||
{
|
{
|
||||||
// 1. All required inputs present?
|
// 1. All required inputs present?
|
||||||
bool inputsOk = true;
|
if (!recipeInputsAvailable(building, *recipe))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1082,26 +1052,8 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build combined materials list (base + modules).
|
// Build combined materials list (base + modules).
|
||||||
std::map<std::string, int> requiredMaterials;
|
const std::map<std::string, int> requiredMaterials =
|
||||||
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
computeShipyardRequiredMaterials(building);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Idle: check if all combined materials are available.
|
// Idle: check if all combined materials are available.
|
||||||
bool inputsOk = true;
|
bool inputsOk = true;
|
||||||
@@ -1328,6 +1280,147 @@ int BuildingSystem::getActiveProductionBuildingCount() const
|
|||||||
return count;
|
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<BuildingSystem::BeltTileInfo> BuildingSystem::getAllBeltTiles() const
|
||||||
{
|
{
|
||||||
std::vector<BeltTileInfo> result;
|
std::vector<BeltTileInfo> result;
|
||||||
|
|||||||
@@ -26,6 +26,18 @@
|
|||||||
|
|
||||||
class Hasher;
|
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
|
// Manages building placement, construction queuing, and the per-tick
|
||||||
// production loop (belt→building pull, production, building→belt push).
|
// production loop (belt→building pull, production, building→belt push).
|
||||||
// All types including Belt and Splitter are stored as Building instances;
|
// 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
|
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
|
||||||
// that currently has an active production cycle.
|
// that currently has an active production cycle.
|
||||||
int getActiveProductionBuildingCount() const;
|
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;
|
std::vector<BeltTileInfo> getAllBeltTiles() const;
|
||||||
bool isTileOccupied(QPoint tile) const;
|
bool isTileOccupied(QPoint tile) const;
|
||||||
|
|
||||||
@@ -200,6 +218,22 @@ private:
|
|||||||
const Port& outputPort,
|
const Port& outputPort,
|
||||||
const Item& item);
|
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 BuildingDef* findBuildingDef(BuildingType type) const;
|
||||||
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
|
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
|
||||||
const ShipDef* findShipDef(const std::string& id) const;
|
const ShipDef* findShipDef(const std::string& id) const;
|
||||||
|
|||||||
@@ -1308,3 +1308,116 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
|
|||||||
REQUIRE(builtInfo->filterA == filterA);
|
REQUIRE(builtInfo->filterA == filterA);
|
||||||
REQUIRE(builtInfo->filterB.empty());
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,6 +124,20 @@ QPoint portBodyTile(QPoint portTile, Rotation direction)
|
|||||||
return portTile;
|
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
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
@@ -1039,6 +1053,35 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
|||||||
port.direction, bv.outline, /*centered*/ false);
|
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.
|
// HP bar below the HQ footprint; the HQ's HP lives on its proxy entity.
|
||||||
if (b.type == BuildingType::Hq)
|
if (b.type == BuildingType::Hq)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -60,6 +60,17 @@ struct ToastVisuals
|
|||||||
int fontSize;
|
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
|
struct VisualsConfig
|
||||||
{
|
{
|
||||||
TileVisuals asteroid;
|
TileVisuals asteroid;
|
||||||
@@ -72,4 +83,5 @@ struct VisualsConfig
|
|||||||
BeamVisuals beams;
|
BeamVisuals beams;
|
||||||
OverlayVisuals overlays;
|
OverlayVisuals overlays;
|
||||||
ToastVisuals toast;
|
ToastVisuals toast;
|
||||||
|
StatusLightVisuals statusLight;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -237,5 +237,15 @@ VisualsConfig VisualsLoader::load(const std::string& path)
|
|||||||
cfg.toast.fontSize = requireInt(t, "font_size", "toast");
|
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;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user