Every display naming an item now shows that item's production tooltip, not only the selection panel's chips: the header block stock, the total cost of a building multi-selection, the remaining scrap of a debris selection, the icons of every recipe summary, and a blueprint card's cost. ItemTooltip moves out of the selection panel and takes an ItemTooltipContext of its own -- an item is named all over the UI, and the panel's context carries visuals and a debug flag no tooltip reads. A recipe line wraps each icon with its amount so the pair can be pointed at as one statement, and attaches tooltips only where asked: a module button and the item tooltip itself draw the same line and stay silent. Hover only wherever the display sits on something the player clicks, whose click is not free to explain. Note that a recipe line on an option button can no longer be transparent to the mouse -- Qt never looks inside a transparent widget for the cursor -- so it relies on press propagation to keep picking the option. The header block stock loses world.building_blocks_tooltip, showing what every other block icon shows instead. The Expand button keeps no tooltip: its cost is painted into its face with nowhere to hang one, and the button is due to be removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
235 lines
8.7 KiB
C++
235 lines
8.7 KiB
C++
#include "BufferedBuildingContent.h"
|
|
|
|
#include <map>
|
|
#include <set>
|
|
|
|
#include <QVBoxLayout>
|
|
|
|
#include "Building.h"
|
|
#include "BuildingTarget.h"
|
|
#include "DisplayName.h"
|
|
#include "FactoryQueries.h"
|
|
#include "ProductionSection.h"
|
|
#include "RecipeLineRow.h"
|
|
#include "SectionBox.h"
|
|
#include "SelectionNames.h"
|
|
#include "Simulation.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
std::vector<RecipeLineRow::Amount> toAmounts(const std::map<std::string, int>& map)
|
|
{
|
|
std::vector<RecipeLineRow::Amount> amounts;
|
|
amounts.reserve(map.size());
|
|
for (const std::pair<const std::string, int>& entry : map)
|
|
{
|
|
amounts.push_back(RecipeLineRow::Amount{ entry.first, entry.second });
|
|
}
|
|
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
|
|
|
|
|
|
BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context,
|
|
BuildingId id, QWidget* parent)
|
|
: SelectionContent(context, asConstructionSite(context, id), parent)
|
|
, m_id(id)
|
|
{
|
|
// The summary is configuration -- what the building will do -- so it sits with the
|
|
// selection control and is shown for a construction site too (REQ-UI-RECIPE-SUMMARY).
|
|
m_recipeSummary = new RecipeLineRow(context.itemIcons, context.buildingIcons, this);
|
|
// Its icons name items, and nothing in the panel is clicked, so each explains itself
|
|
// on hover or click -- which is how the player follows an input back to what makes it
|
|
// without that item being in a buffer (REQ-UI-ITEM-VALUE-TOOLTIP).
|
|
m_recipeSummary->setItemTooltips(context.getItemTooltipContext(),
|
|
TooltipTrigger::Trigger::HoverAndClick);
|
|
getConfigurationLayout()->addWidget(m_recipeSummary);
|
|
|
|
m_inputSection = new SectionBox(tr("Input buffers"), this);
|
|
m_inputChips = new ItemChipRow(context, m_inputSection);
|
|
m_inputSection->getContentLayout()->addWidget(m_inputChips);
|
|
|
|
m_production = new ProductionSection(this);
|
|
|
|
m_outputSection = new SectionBox(tr("Output buffers"), this);
|
|
m_outputChips = new ItemChipRow(context, 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()
|
|
{
|
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
|
if (!target.isValid())
|
|
{
|
|
// Gone under the card. SelectionPanel rebuilds on the same refresh; this only
|
|
// has to avoid reading it.
|
|
return;
|
|
}
|
|
|
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
|
|
|
// The card already names the building in its header, so the summary states the cycle
|
|
// alone -- no building chip, no recipe name (REQ-UI-RECIPE-SUMMARY).
|
|
const CycleInfo cycle = getCycleInfo(target);
|
|
RecipeLineRow::Spec summary;
|
|
summary.inputs = toAmounts(cycle.perCycleInputs);
|
|
summary.outputGroups = cycle.perCycleOutputGroups;
|
|
summary.durationSeconds = cycle.durationSeconds;
|
|
m_recipeSummary->setLine(summary);
|
|
|
|
refreshControls(target);
|
|
}
|
|
|
|
void BufferedBuildingContent::refreshRuntime()
|
|
{
|
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
|
if (!target.building)
|
|
{
|
|
return;
|
|
}
|
|
|
|
setProductionStatusSlot(*target.building);
|
|
|
|
const CycleInfo cycle = getCycleInfo(target);
|
|
|
|
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]++;
|
|
}
|
|
}
|
|
|
|
// A chip stands for a buffer, and a buffer exists for every item any group can
|
|
// produce (REQ-MAT-OUTPUT-BUFFER), so the groups are flattened here.
|
|
std::map<std::string, int> producible;
|
|
for (const std::vector<RecipeLineRow::Amount>& group : cycle.perCycleOutputGroups)
|
|
{
|
|
for (const RecipeLineRow::Amount& amount : group)
|
|
{
|
|
producible[amount.itemId] = std::max(producible[amount.itemId], amount.amount);
|
|
}
|
|
}
|
|
|
|
std::vector<ItemChipRow::Entry> entries;
|
|
for (const std::string& itemId :
|
|
collectItemIds(buffered, producible, cycle.handledOutputs))
|
|
{
|
|
if (!getContext().sim->isItemUnlocked(itemId)) { continue; }
|
|
|
|
ItemChipRow::Entry chip;
|
|
chip.itemId = itemId;
|
|
// Counted against this item's own buffer capacity, which is what production
|
|
// stops at (REQ-MAT-OUTPUT-BUFFER, REQ-UI-SINGLE-SELECTION). A chip for an item
|
|
// the building has no buffer for -- one left over from a previous recipe --
|
|
// carries the bare count, as an unsized buffer has no denominator to state.
|
|
const std::map<ItemType, int>::const_iterator capIt =
|
|
building.outputBuffer.caps.find(ItemType{itemId});
|
|
const int cap =
|
|
(capIt != building.outputBuffer.caps.end()) ? capIt->second : 0;
|
|
chip.countText = cap > 0
|
|
? tr("%1 / %2").arg(lookUp(buffered, itemId)).arg(cap)
|
|
: QString::number(lookUp(buffered, itemId));
|
|
chip.subLine = QString::fromStdString(toDisplayName(itemId));
|
|
entries.push_back(chip);
|
|
}
|
|
return entries;
|
|
}
|