4 Commits

Author SHA1 Message Date
aeab16757f keep an auto-recipe building's summary between its cycles
A Smelter's recipe summary was dropped whenever it sat between cycles and shown
again when the next one started, as REQ-UI-RECIPE-SUMMARY asked. It is the
widest row of the card and a row of its own height, so the panel changed width
and height every time the building started or stopped -- which is exactly when
its status caption changes, and why the two looked connected.

It now keeps describing the recipe it ran last until another one runs, which
also holds the input chips' per-cycle amounts still instead of letting them
fall back to bare item names each time. A building that has never run a cycle
still has nothing to describe and shows no summary.

A Miner or Assembler never had this: its recipe is player-selected and outlives
the cycle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-09 14:32:24 +02:00
b97347329f measure the card against every one of its layouts
Changing a label's text posts a LayoutRequest to the widget holding it, and
that event is only delivered when the event loop next runs. The panel measured
its card by invalidating the body's own layout alone, so any layout nested
inside the card -- which is all of them -- still answered with the width of the
text before the change. The panel therefore placed itself against the previous
values and corrected itself on the following refresh, a frame late.

Measured on a BarRow: with the value going idle, 0%, 42%, 100% the panel's
measurement reported 16, 16, 17, 23 where the settled widths were 16, 17, 23,
29 -- one change behind throughout. Re-activating every layout in the card
gives the settled answer without waiting for the event loop.

This is not the size change the player reported, which I could not reproduce
here; it is a second, quieter one found while looking for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-09 14:16:23 +02:00
352beda47c let the world renderer hold the simulation by const reference
The renderer documented itself as reading the simulation and never writing it,
while holding a mutable reference to it -- because hasAll() forced that on
every caller. With hasAll const the claim and the type can agree, and the const
overloads of getAdmin, forEach and get cover everything the renderer does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-08 19:42:24 +02:00
256c4b5492 make EntityAdmin::hasAll const
Asking whether an entity has components does not write to the registry, and
entt's all_of is const. Callers that only read were forced to take the admin --
and through it the whole simulation -- by non-const reference to ask; the
selection bounds helpers now say what they mean.

Widening a member to const breaks nothing: every existing caller still binds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-08 19:31:33 +02:00
10 changed files with 62 additions and 27 deletions

View File

@@ -571,12 +571,12 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
- an **input** chip shows the per-cycle amount below the count (the items consumed per run, e.g. `/ 2 per cycle`), or the count alone when the building has no selected recipe or schematic to give one;
- an **output** chip shows the count against the output buffer's capacity as `a / b` (REQ-MAT-OUTPUT-BUFFER), with the item's name below.
Input and output chips form separately captioned sections, each shown only while it holds something (REQ-UI-SELECTION-CARD). The production section (REQ-UI-PRODUCTION-PROGRESS) sits **between them**, so the card reads in the direction the materials flow: what goes in, what is being made of it, what has come out. For a selected construction site the buffer sections are omitted (REQ-BLD-SITE-CONFIG).
Input and output chips form separately captioned sections (REQ-UI-SELECTION-CARD). A section lists a chip for **every item the building's cycle involves**, and for an auto-recipe building every item it handles at all, whether or not the buffer currently holds any: an empty buffer reads `0` rather than its chip disappearing, so the card keeps one shape while the building runs. A section left with no chips at all is not shown. The production section (REQ-UI-PRODUCTION-PROGRESS) sits **between them**, so the card reads in the direction the materials flow: what goes in, what is being made of it, what has come out. For a selected construction site the buffer sections are omitted (REQ-BLD-SITE-CONFIG).
**Only unlocked items are listed.** A building's buffers may carry entries for items the player cannot make yet — an auto-recipe building's buffers are sized over *every* recipe of its type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), including recipes that are still locked. Those entries are left out of both sections, consistent with the rest of the UI hiding what is not unlocked yet (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SPLITTER), so a Smelter shows the ores it can actually smelt rather than every ore in the game.
**An idle auto-recipe building still shows what it handles.** Having no selected recipe (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), it would otherwise show empty sections whenever it happens to be between cycles. Its input and output sections instead list the unlocked items of every recipe of its type — the same union its buffers were sized over — with a count and no per-cycle denominator, since no one recipe is in force. While a cycle is running, that cycle's recipe supplies the denominators as for any other building.
- REQ-UI-RECIPE-SUMMARY: Below the recipe/schematic selection control, a building running a recipe or schematic shows a one-line **recipe summary**: each input item's icon with its per-cycle amount, an arrow, each output item's icon with its per-cycle amount, and the cycle time in seconds. It restates what the building will do without opening the selection dialog, and it is the panel's only display of the cycle time. For a Shipyard the summary is built from the schematic's materials and production time including the placed modules' contributions (REQ-BLD-SHIPYARD, REQ-MOD-STAT-CALC), matching the buffers beneath it. Auto-recipe buildings (Smelter, Reprocessing Plant — REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) have no player-selected recipe and so show no selection control; they show the summary of the recipe currently in production, and none while idle. A building with no recipe or schematic selected shows no summary.
- REQ-UI-RECIPE-SUMMARY: Below the recipe/schematic selection control, a building running a recipe or schematic shows a one-line **recipe summary**: each input item's icon with its per-cycle amount, an arrow, each output item's icon with its per-cycle amount, and the cycle time in seconds. It restates what the building will do without opening the selection dialog, and it is the panel's only display of the cycle time. For a Shipyard the summary is built from the schematic's materials and production time including the placed modules' contributions (REQ-BLD-SHIPYARD, REQ-MOD-STAT-CALC), matching the buffers beneath it. Auto-recipe buildings (Smelter, Reprocessing Plant — REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) have no player-selected recipe and so show no selection control; they show the summary of the recipe currently in production and, while between cycles, of the one they ran last. They keep it rather than dropping it, because a summary that came and went with each cycle would resize the card in step with the building's status (REQ-UI-SELECTION-STATUS), which is the one thing the panel must not do while the player is reading it (REQ-UI-SELECTION-PANEL). Such a building shows no summary only until it has run its first cycle. A building with no recipe or schematic selected shows no summary.
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the panel's runtime group shows a captioned **production section** between the input and output buffer sections (REQ-UI-SINGLE-SELECTION): a horizontal progress bar filled to the completion of the active production cycle, with that completion beside the caption as an integer percentage (e.g. `72%`), or the text `idle` in place of the percentage and an empty bar when no production cycle is active. The cycle time is shown in the recipe summary (REQ-UI-RECIPE-SUMMARY) rather than repeated here. When no recipe or schematic is selected, the production section is not shown at all.
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected and the selection does not aggregate (REQ-UI-SELECTION-AGGREGATE), the panel shows a count summary. Its header names the size of the selection as `<n> buildings` in place of an object name, and carries no symbol and nothing in its right slot. Below it is one row per selected building type — the type's symbol, its name, and the number selected as `x<count>` — one type per row, and no per-building detail. A final row shows the **total building block cost** of the selection, captioned `Total cost` with the value followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON): the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button bar); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-CARD). Construction sites count at their building type's full placement cost regardless of construction progress.

View File

@@ -28,7 +28,7 @@ public:
void forEach(Func&& f) const;
template <typename... Ts>
bool hasAll(entt::entity entity);
bool hasAll(entt::entity entity) const;
template <typename T>
T& get(entt::entity entity);
@@ -101,7 +101,7 @@ void EntityAdmin::forEach(Func&& f) const
}
template <typename... Ts>
bool EntityAdmin::hasAll(entt::entity entity)
bool EntityAdmin::hasAll(entt::entity entity) const
{
return m_registry.all_of<Ts...>(entity);
}

View File

@@ -42,7 +42,7 @@ std::optional<QRectF> getBuildingWidgetRect(const FactoryState& state,
return std::nullopt;
}
std::optional<QRectF> getActorWidgetRect(EntityAdmin& admin,
std::optional<QRectF> getActorWidgetRect(const EntityAdmin& admin,
const WorldCoordinates& coordinates,
entt::entity actor)
{
@@ -68,7 +68,7 @@ std::optional<QRectF> getActorWidgetRect(EntityAdmin& admin,
return std::nullopt;
}
std::optional<QRectF> getDebrisWidgetRect(EntityAdmin& admin,
std::optional<QRectF> getDebrisWidgetRect(const EntityAdmin& admin,
const WorldCoordinates& coordinates,
entt::entity debris)
{
@@ -82,7 +82,7 @@ std::optional<QRectF> getDebrisWidgetRect(EntityAdmin& admin,
return QRectF(center.x() - radius, center.y() - radius, 2.0 * radius, 2.0 * radius);
}
QRect getSelectionWidgetRect(Simulation& sim, const WorldCoordinates& coordinates,
QRect getSelectionWidgetRect(const Simulation& sim, const WorldCoordinates& coordinates,
const std::vector<BuildingId>& buildings,
const std::vector<entt::entity>& actors,
const std::vector<entt::entity>& debris)

View File

@@ -29,22 +29,20 @@ std::optional<QRectF> getBuildingWidgetRect(const FactoryState& state,
BuildingId id);
// The body of a ship or of a defence station (REQ-UI-ENTITY-CLICK-SELECT): a station's
// footprint, or the square the ship's triangle is drawn in. The admin is taken by
// non-const reference only because EntityAdmin::hasAll() is not const; nothing here
// writes to it.
std::optional<QRectF> getActorWidgetRect(EntityAdmin& admin,
// footprint, or the square the ship's triangle is drawn in.
std::optional<QRectF> getActorWidgetRect(const EntityAdmin& admin,
const WorldCoordinates& coordinates,
entt::entity actor);
// The circle a piece of debris is drawn as (REQ-UI-DEBRIS-CLICK-SELECT).
std::optional<QRectF> getDebrisWidgetRect(EntityAdmin& admin,
std::optional<QRectF> getDebrisWidgetRect(const EntityAdmin& admin,
const WorldCoordinates& coordinates,
entt::entity debris);
// The rectangle covering a whole selection -- one object, or the bounding box of all of
// them (REQ-UI-SELECTION-PANEL). Objects that no longer resolve are skipped; the result
// is null when none of them does.
QRect getSelectionWidgetRect(Simulation& sim, const WorldCoordinates& coordinates,
QRect getSelectionWidgetRect(const Simulation& sim, const WorldCoordinates& coordinates,
const std::vector<BuildingId>& buildings,
const std::vector<entt::entity>& actors,
const std::vector<entt::entity>& debris);

View File

@@ -1,5 +1,7 @@
#include "SelectionPanel.h"
#include <QLayout>
#include <QList>
#include <QScrollArea>
#include <QScrollBar>
#include <QVBoxLayout>
@@ -263,9 +265,16 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
// laid out, does the width it reports. Measuring at whatever width the panel
// happens to have carries the previous card's shape into this one.
//
// Each measurement re-runs the body layout. Cards are built and discarded whole, so
// its cached hint describes the card before this one until it is invalidated, and
// re-running it is also what accounts for the parts a card hides and shows as it
// Each measurement re-runs the card's layouts -- every one of them, not just the
// body's own. Changing a label's text posts a LayoutRequest to the widget holding it
// and that event is only delivered when the event loop next runs, so a layout nested
// inside the card still reports the width of the text before the change: measuring
// here and re-measuring on the following refresh then gives two different answers,
// and the panel visibly resizes a frame after its content changed. Re-activating
// them all is what a delivered LayoutRequest would have done.
//
// Cards are built and discarded whole, so this is also what discards the previous
// card's cached hints, and what accounts for the parts a card hides and shows as it
// refreshes. The polish belongs to the same step: a freshly created chip reports an
// unstyled hint until the stylesheet has reached it, and the chips carry border and
// padding that change their size.
@@ -273,6 +282,16 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
{
m_body->resize(widthPx, m_body->height());
m_body->ensurePolished();
// Deepest first, so no layout is re-activated from children that are themselves
// still stale. findChildren walks parents before children, hence the reverse.
const QList<QLayout*> nested = m_body->findChildren<QLayout*>();
for (QList<QLayout*>::const_reverse_iterator it = nested.rbegin();
it != nested.rend(); ++it)
{
(*it)->invalidate();
(*it)->activate();
}
m_body->layout()->invalidate();
m_body->layout()->activate();
return m_body->sizeHint();

View File

@@ -109,7 +109,7 @@ QColor statusLightFill(ProductionStatus status, const StatusLightVisuals& sl)
} // namespace
WorldRenderer::WorldRenderer(Simulation& sim, const VisualsConfig& visuals,
WorldRenderer::WorldRenderer(const Simulation& sim, const VisualsConfig& visuals,
ItemIconCache* itemIcons, const std::string& configDir)
: m_sim(sim)
, m_visuals(visuals)

View File

@@ -77,7 +77,7 @@ public:
// `itemIcons` is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); not
// owned, must outlive this renderer. `configDir` is used once, to load the
// per-building world icons.
WorldRenderer(Simulation& sim, const VisualsConfig& visuals,
WorldRenderer(const Simulation& sim, const VisualsConfig& visuals,
ItemIconCache* itemIcons, const std::string& configDir);
~WorldRenderer();
@@ -141,9 +141,8 @@ private:
std::optional<QVector2D> entityPosition(entt::entity entity) const;
// Non-const only because EntityAdmin's component accessors are; the renderer
// reads the simulation and never writes it.
Simulation& m_sim;
// The renderer reads the simulation and never writes it.
const Simulation& m_sim;
const VisualsConfig& m_visuals;
// Per-item icon cache (REQ-UI-ITEM-ICON), shared window-wide and owned by

View File

@@ -41,13 +41,22 @@ BufferedBuildingContent::CycleInfo AutoProductionContent::getCycleInfo(
}
}
if (!target.building || !target.building->production.has_value())
// Which recipe describes the cycle: the one running, or -- between cycles -- the one
// that ran last. Dropping it while idle would take the summary row and the chips'
// per-cycle amounts away and bring them back with every cycle, resizing the card in
// step with the building's status (REQ-UI-RECIPE-SUMMARY). Only a building that has
// never run has nothing to describe.
if (target.building && target.building->production.has_value())
{
m_lastRecipeId = target.building->production->recipeId;
}
if (!target.building || m_lastRecipeId.empty())
{
return info;
}
const RecipeDef* recipe = getContext().config->recipes.findRecipeDef(
target.building->production->recipeId, target.type);
const RecipeDef* recipe =
getContext().config->recipes.findRecipeDef(m_lastRecipeId, target.type);
if (!recipe)
{
return info;

View File

@@ -1,12 +1,14 @@
#pragma once
#include <string>
#include "BufferedBuildingContent.h"
#include "SelectionContentFactory.h"
// The card for a Smelter or a Reprocessing Plant (REQ-UI-SELECTION-CONTENT). Both
// auto-process whatever they receive and have no player-facing recipe selection
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), so the card has no configuration group at
// all; its cycle is whichever recipe is currently in production.
// all; its cycle is whichever recipe is in production, or the last one that was.
class AutoProductionContent : public BufferedBuildingContent
{
Q_OBJECT
@@ -17,4 +19,11 @@ public:
protected:
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
private:
// The recipe last seen in production, which keeps describing the cycle while the
// building sits between cycles (REQ-UI-RECIPE-SUMMARY). Mutable because it is a
// record of what getCycleInfo() has observed rather than state of its own: the card
// shows the same thing whether or not it has been asked before.
mutable std::string m_lastRecipeId;
};

View File

@@ -44,8 +44,9 @@ protected:
// REQ-BLD-SALVAGE-BAY) or has no recipe or schematic selected yet: the
// production section is then not shown (REQ-UI-PRODUCTION-PROGRESS).
bool runsProduction = false;
// 0 while an auto-recipe building sits between cycles, when no single recipe
// names a cycle time; the progress line then reads "idle".
// 0 only when no recipe describes the cycle at all -- an auto-recipe building
// that has yet to run one (REQ-UI-RECIPE-SUMMARY). The progress line reads
// "idle" whenever no cycle is actually running, whatever this says.
double durationSeconds = 0.0;
};