fix selection panel contents

This commit is contained in:
2026-08-07 22:01:10 +02:00
parent 9f574fa05e
commit 7ae5f8c4dc
10 changed files with 204 additions and 165 deletions

View File

@@ -3,6 +3,7 @@
#include "Building.h"
#include "BuildingTarget.h"
#include "GameConfig.h"
#include "ProductionRules.h"
AutoProductionContent::AutoProductionContent(const SelectionContext& context,
const SelectionRequest& request,
@@ -19,6 +20,27 @@ BufferedBuildingContent::CycleInfo AutoProductionContent::getCycleInfo(
// REQ-BLD-REPROCESSING), so its production section is always shown -- but only a
// running cycle names a recipe, so while it is idle there is no cycle to describe.
info.runsProduction = true;
if (target.building)
{
// What the building handles at all, so its buffers are not blank whenever it
// happens to be between cycles (REQ-UI-SINGLE-SELECTION). This is the same union
// of every recipe of its type that the simulation sized the buffers over, and
// the locked ones among them are dropped when the card lists them.
for (const RecipeDef* recipe :
gatherCandidateRecipes(*getContext().config, *target.building))
{
for (const RecipeIngredient& ingredient : recipe->inputs)
{
info.handledInputs.push_back(ingredient.item);
}
for (const RecipeOutput& output : recipe->outputs)
{
info.handledOutputs.push_back(output.item);
}
}
}
if (!target.building || !target.building->production.has_value())
{
return info;

View File

@@ -50,9 +50,14 @@ public:
protected:
void paintEvent(QPaintEvent* /*event*/) override
{
// The active group is asked for by name rather than taken from the current one,
// which follows the window's focus: the inactive group's highlight is close
// enough to the card's background that the bar reads as gone whenever the game
// is tabbed away from or a modal dialog holds focus -- exactly when a player
// watching a build finish is most likely to be looking at it.
const QColor fill = m_fillColor.isValid()
? m_fillColor
: palette().color(QPalette::Highlight);
: palette().color(QPalette::Active, QPalette::Highlight);
QColor track = fill;
track.setAlpha(kTrackAlpha);

View File

@@ -1,103 +0,0 @@
#include "BufferSection.h"
#include <vector>
#include <QVBoxLayout>
#include "Building.h"
#include "DisplayName.h"
#include "ItemChipRow.h"
#include "SectionBox.h"
namespace
{
int findPerCycle(const std::map<std::string, int>& perCycle, const std::string& itemId)
{
const std::map<std::string, int>::const_iterator it = perCycle.find(itemId);
return (it != perCycle.end()) ? it->second : 0;
}
} // namespace
BufferSection::BufferSection(const SelectionContext& context, QWidget* parent)
: QWidget(parent)
{
QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(6);
m_inputSection = new SectionBox(tr("Input buffers"), this);
m_inputChips = new ItemChipRow(context.itemIcons, m_inputSection);
m_inputSection->getContentLayout()->addWidget(m_inputChips);
m_outputSection = new SectionBox(tr("Output buffer"), this);
m_outputChips = new ItemChipRow(context.itemIcons, m_outputSection);
m_outputSection->getContentLayout()->addWidget(m_outputChips);
layout->addWidget(m_inputSection);
layout->addWidget(m_outputSection);
}
void BufferSection::setBuffers(const Building& building,
const std::map<std::string, int>& perCycleInputs,
const std::map<std::string, int>& perCycleOutputs)
{
std::vector<ItemChipRow::Entry> inputs;
for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
{
ItemChipRow::Entry chip;
chip.itemId = entry.first.id;
chip.countText = QString::number(entry.second);
const int perCycle = findPerCycle(perCycleInputs, entry.first.id);
if (perCycle > 0)
{
chip.subLine = tr("/ %1 per cycle").arg(perCycle);
}
inputs.push_back(chip);
}
// Output-side items are the buffered ones plus those still emerging onto the output
// belts: an emerging item still belongs to the output buffer (REQ-MAT-OUTPUT-EMERGE),
// so leaving it out would make it vanish from the panel while it animates.
std::map<std::string, int> outputCounts;
for (const Item& item : building.outputBuffer.items)
{
outputCounts[item.type.id]++;
}
for (const std::vector<BeltItemSlot>& lane : building.emergingItems)
{
for (const BeltItemSlot& slot : lane)
{
outputCounts[slot.item.type.id]++;
}
}
// A configured building lists everything its cycle produces, so an output the player
// is waiting for reads as 0 rather than being absent.
for (const std::pair<const std::string, int>& entry : perCycleOutputs)
{
outputCounts.emplace(entry.first, 0);
}
std::vector<ItemChipRow::Entry> outputs;
for (const std::pair<const std::string, int>& entry : outputCounts)
{
ItemChipRow::Entry chip;
chip.itemId = entry.first;
// Counted against the buffer's capacity, which is what production stops at
// (REQ-MAT-OUTPUT-BUFFER).
chip.countText = building.outputBuffer.capacity > 0
? tr("%1 / %2").arg(entry.second).arg(building.outputBuffer.capacity)
: QString::number(entry.second);
chip.subLine = QString::fromStdString(toDisplayName(entry.first));
outputs.push_back(chip);
}
m_inputChips->setEntries(inputs);
m_outputChips->setEntries(outputs);
m_inputSection->setVisible(!inputs.empty());
m_outputSection->setVisible(!outputs.empty());
setVisible(!inputs.empty() || !outputs.empty());
}

View File

@@ -1,39 +0,0 @@
#pragma once
#include <map>
#include <string>
#include <QWidget>
#include "SelectionContext.h"
struct Building;
class ItemChipRow;
class SectionBox;
// The input and output buffer contents of one building, each a captioned section of item
// chips (REQ-UI-SINGLE-SELECTION).
//
// Counting what is in the buffers is the same for every building type, so it happens
// here; what a cycle consumes and produces is not, so the owning content supplies those
// per-cycle amounts. An item with no entry in the maps is shown without a denominator,
// and a section holding nothing is not shown at all.
class BufferSection : public QWidget
{
Q_OBJECT
public:
explicit BufferSection(const SelectionContext& context, QWidget* parent = nullptr);
// perCycleInputs and perCycleOutputs map an item id to the amount one production
// cycle consumes or produces. Both may be empty, for a building that runs no cycle.
void setBuffers(const Building& building,
const std::map<std::string, int>& perCycleInputs,
const std::map<std::string, int>& perCycleOutputs);
private:
SectionBox* m_inputSection;
ItemChipRow* m_inputChips;
SectionBox* m_outputSection;
ItemChipRow* m_outputChips;
};

View File

@@ -1,15 +1,17 @@
#include "BufferedBuildingContent.h"
#include <vector>
#include <map>
#include <set>
#include <QVBoxLayout>
#include "Building.h"
#include "BufferSection.h"
#include "BuildingTarget.h"
#include "DisplayName.h"
#include "FactoryQueries.h"
#include "ProductionSection.h"
#include "RecipeSummaryRow.h"
#include "SectionBox.h"
#include "SelectionNames.h"
#include "Simulation.h"
@@ -27,6 +29,33 @@ std::vector<RecipeSummaryRow::Amount> toAmounts(const std::map<std::string, int>
return amounts;
}
// Every item one side of the card should list: what the buffer holds, what a cycle
// moves, and what the building handles at all. A building's buffers can carry items it
// is not currently making anything of, and an auto-recipe building between cycles names
// nothing at all, so the three sources are unioned rather than one being picked.
std::set<std::string> collectItemIds(const std::map<std::string, int>& buffered,
const std::map<std::string, int>& perCycle,
const std::vector<std::string>& handled)
{
std::set<std::string> itemIds;
for (const std::pair<const std::string, int>& entry : buffered)
{
itemIds.insert(entry.first);
}
for (const std::pair<const std::string, int>& entry : perCycle)
{
itemIds.insert(entry.first);
}
itemIds.insert(handled.begin(), handled.end());
return itemIds;
}
int lookUp(const std::map<std::string, int>& map, const std::string& key)
{
const std::map<std::string, int>::const_iterator it = map.find(key);
return (it != map.end()) ? it->second : 0;
}
} // namespace
@@ -40,10 +69,21 @@ BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context
m_recipeSummary = new RecipeSummaryRow(context.itemIcons, this);
getConfigurationLayout()->addWidget(m_recipeSummary);
m_buffers = new BufferSection(context, this);
m_inputSection = new SectionBox(tr("Input buffers"), this);
m_inputChips = new ItemChipRow(context.itemIcons, m_inputSection);
m_inputSection->getContentLayout()->addWidget(m_inputChips);
m_production = new ProductionSection(this);
getRuntimeLayout()->addWidget(m_buffers);
m_outputSection = new SectionBox(tr("Output buffer"), this);
m_outputChips = new ItemChipRow(context.itemIcons, m_outputSection);
m_outputSection->getContentLayout()->addWidget(m_outputChips);
// In the direction the materials flow: what goes in, what is being made of it, what
// has come out (REQ-UI-SINGLE-SELECTION, REQ-UI-PRODUCTION-PROGRESS).
getRuntimeLayout()->addWidget(m_inputSection);
getRuntimeLayout()->addWidget(m_production);
getRuntimeLayout()->addWidget(m_outputSection);
}
void BufferedBuildingContent::refreshConfiguration()
@@ -77,8 +117,93 @@ void BufferedBuildingContent::refreshRuntime()
setProductionStatusSlot(*target.building);
const CycleInfo cycle = getCycleInfo(target);
m_buffers->setBuffers(*target.building, cycle.perCycleInputs, cycle.perCycleOutputs);
const std::vector<ItemChipRow::Entry> inputs =
buildInputEntries(*target.building, cycle);
const std::vector<ItemChipRow::Entry> outputs =
buildOutputEntries(*target.building, cycle);
m_inputChips->setEntries(inputs);
m_outputChips->setEntries(outputs);
m_inputSection->setVisible(!inputs.empty());
m_outputSection->setVisible(!outputs.empty());
m_production->setProduction(cycle.runsProduction, *target.building,
cycle.durationSeconds,
getContext().sim->getCurrentTick());
}
std::vector<ItemChipRow::Entry> BufferedBuildingContent::buildInputEntries(
const Building& building, const CycleInfo& cycle) const
{
std::map<std::string, int> buffered;
for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
{
buffered[entry.first.id] = entry.second;
}
std::vector<ItemChipRow::Entry> entries;
for (const std::string& itemId :
collectItemIds(buffered, cycle.perCycleInputs, cycle.handledInputs))
{
// An auto-recipe building's buffers are sized over every recipe of its type,
// including recipes still locked, so those entries are left out here
// (REQ-UI-SINGLE-SELECTION, REQ-LOCK-UI-RECIPE).
if (!getContext().sim->isItemUnlocked(itemId)) { continue; }
ItemChipRow::Entry chip;
chip.itemId = itemId;
chip.countText = QString::number(lookUp(buffered, itemId));
const int perCycle = lookUp(cycle.perCycleInputs, itemId);
if (perCycle > 0)
{
chip.subLine = tr("/ %1 per cycle").arg(perCycle);
}
else
{
chip.subLine = QString::fromStdString(toDisplayName(itemId));
}
entries.push_back(chip);
}
return entries;
}
std::vector<ItemChipRow::Entry> BufferedBuildingContent::buildOutputEntries(
const Building& building, const CycleInfo& cycle) const
{
// The buffered items plus those still emerging onto the output belts: an emerging
// item still belongs to the output buffer (REQ-MAT-OUTPUT-EMERGE), so leaving it out
// would make it vanish from the panel while it animates.
std::map<std::string, int> buffered;
for (const Item& item : building.outputBuffer.items)
{
buffered[item.type.id]++;
}
for (const std::vector<BeltItemSlot>& lane : building.emergingItems)
{
for (const BeltItemSlot& slot : lane)
{
buffered[slot.item.type.id]++;
}
}
std::vector<ItemChipRow::Entry> entries;
for (const std::string& itemId :
collectItemIds(buffered, cycle.perCycleOutputs, cycle.handledOutputs))
{
if (!getContext().sim->isItemUnlocked(itemId)) { continue; }
ItemChipRow::Entry chip;
chip.itemId = itemId;
// Counted against the buffer's capacity, which is what production stops at
// (REQ-MAT-OUTPUT-BUFFER).
chip.countText = building.outputBuffer.capacity > 0
? tr("%1 / %2").arg(lookUp(buffered, itemId))
.arg(building.outputBuffer.capacity)
: QString::number(lookUp(buffered, itemId));
chip.subLine = QString::fromStdString(toDisplayName(itemId));
entries.push_back(chip);
}
return entries;
}

View File

@@ -2,20 +2,23 @@
#include <map>
#include <string>
#include <vector>
#include "BuildingId.h"
#include "ItemChipRow.h"
#include "SelectionContent.h"
struct Building;
struct BuildingTarget;
class BufferSection;
class ProductionSection;
class RecipeSummaryRow;
class SectionBox;
// Shared body of the four cards that show one building with buffers -- the Miner and
// Assembler, the Smelter and Reprocessing Plant, the Shipyard, and the Salvage Bay
// (REQ-UI-SELECTION-CONTENT). All four show the same header identity and status, recipe
// summary, buffer contents and production progress; they differ only in what one
// production cycle costs and how long it takes, which is what the subclass supplies.
// (REQ-UI-SELECTION-CONTENT). All four show the same header status, recipe summary,
// buffer contents and production progress; they differ only in what one production cycle
// costs and how long it takes, which is what the subclass supplies.
//
// This is implementation sharing, not a catalog entry: every concrete subclass is one
// row of the content catalog.
@@ -29,6 +32,14 @@ protected:
{
std::map<std::string, int> perCycleInputs;
std::map<std::string, int> perCycleOutputs;
// Items the card lists whether or not they are currently in the buffers, for a
// building whose recipe is implicit and so has nothing to name while it sits
// between cycles (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). They carry no
// per-cycle denominator, since no one recipe is in force.
std::vector<std::string> handledInputs;
std::vector<std::string> handledOutputs;
// False when the building produces nothing at all (the Salvage Bay,
// REQ-BLD-SALVAGE-BAY) or has no recipe or schematic selected yet: the
// production section is then not shown (REQ-UI-PRODUCTION-PROGRESS).
@@ -55,8 +66,20 @@ private:
void refreshConfiguration() override;
void refreshRuntime() override;
BuildingId m_id;
RecipeSummaryRow* m_recipeSummary;
BufferSection* m_buffers;
std::vector<ItemChipRow::Entry> buildInputEntries(const Building& building,
const CycleInfo& cycle) const;
std::vector<ItemChipRow::Entry> buildOutputEntries(const Building& building,
const CycleInfo& cycle) const;
BuildingId m_id;
RecipeSummaryRow* m_recipeSummary;
// Input buffers, production progress, output buffer -- in that order, so the card
// reads the way the materials flow (REQ-UI-SINGLE-SELECTION).
SectionBox* m_inputSection;
ItemChipRow* m_inputChips;
ProductionSection* m_production;
SectionBox* m_outputSection;
ItemChipRow* m_outputChips;
};

View File

@@ -15,7 +15,6 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.h
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.h
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.h
${CMAKE_CURRENT_SOURCE_DIR}/BufferSection.h
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.h
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.h
@@ -51,7 +50,6 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.cpp
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BufferSection.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.cpp
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.cpp

View File

@@ -20,14 +20,15 @@ HqContent::HqContent(const SelectionContext& context, const SelectionRequest& re
// (REQ-BLD-DECONSTRUCT), so it is never a construction site.
: SelectionContent(context, std::nullopt, parent)
{
m_hpBar = new BarRow(tr("HP"), this);
m_stockSection = new SectionBox(tr("Building blocks"), this);
m_stockChips = new ItemChipRow(context.itemIcons, m_stockSection);
m_stockSection->getContentLayout()->addWidget(m_stockChips);
m_hpBar = new BarRow(tr("HP"), this);
getRuntimeLayout()->addWidget(m_stockSection);
// HP first, as on every card that has it (REQ-UI-SELECTION-CARD, REQ-UI-HQ-PANEL).
getRuntimeLayout()->addWidget(m_hpBar);
getRuntimeLayout()->addWidget(m_stockSection);
setBuildingIdentity(BuildingType::Hq, getBuildingTypeName(BuildingType::Hq));
}

View File

@@ -96,7 +96,10 @@ SelectionContent::SelectionContent(const SelectionContext& context,
m_constructionSection->getContentLayout()->addWidget(m_constructionBar);
m_constructionSection->getContentLayout()->addWidget(
new EmptyNote(tr("No buffers until built"), m_constructionSection));
cardLayout->addWidget(m_constructionSection);
// Directly below the header rather than where the runtime group sits, so how far
// along the site is reads before what it is configured to become
// (REQ-UI-SELECTION-CARD).
cardLayout->insertWidget(1, m_constructionSection);
setSlot(QColor(), tr("constructing"));
}