21 Commits

Author SHA1 Message Date
09858f4b62 keep an auto-recipe building's summary between its cycles 2026-08-09 20:41:48 +02:00
ca49b9dbec let the world renderer hold the simulation by const reference 2026-08-09 20:37:15 +02:00
79650ae211 make EntityAdmin::hasAll const 2026-08-09 20:37:09 +02:00
c0b009c548 place the selection panel beside what it describes 2026-08-09 20:36:58 +02:00
7ae5f8c4dc fix selection panel contents 2026-08-07 22:01:10 +02:00
9f574fa05e fix issue where the selection panel sometimes collapses to very small size 2026-08-07 21:17:53 +02:00
e5d689c8d8 document the player-input design in architecture.md 2026-08-07 20:43:12 +02:00
0b8f5eccd6 scroll the controls panel's rows instead of clipping them 2026-08-07 20:42:56 +02:00
40415e663b put the controls panel in the corner and let it step around the build bar 2026-08-07 20:42:41 +02:00
7a098edf72 mark the exit row of the controls panel red on its chips instead of its label 2026-08-07 19:52:51 +02:00
365641feb7 size the controls panel to the rows it is actually showing 2026-08-07 19:38:10 +02:00
4b5e288632 record that additive selection is offered only once something is selected 2026-08-07 19:10:26 +02:00
cab7244b00 divide the always-available rows in the General context too 2026-08-07 19:10:04 +02:00
980fad272f show the available controls in a panel over the world 2026-08-07 18:48:01 +02:00
68a43a7d98 resolve mouse gestures through the action table too 2026-08-07 18:47:43 +02:00
1f754de431 resolve keyboard shortcuts through one action table instead of a switch 2026-08-07 18:47:29 +02:00
f61f0bf761 add requirements for a context-sensitive controls panel floating over the game world 2026-08-07 18:47:15 +02:00
075e44d295 give the selection cards their own parts instead of label blobs 2026-08-07 18:43:23 +02:00
2289277e12 split the selection panel into one card per kind of selection 2026-08-07 18:38:55 +02:00
cc0ef856e3 make asteroid smaller 2026-08-06 22:14:02 +02:00
603f8063e9 float the selection panel over the game world instead of a side column 2026-08-06 22:06:42 +02:00
9 changed files with 20 additions and 129 deletions

View File

@@ -1,5 +1,5 @@
[world] [world]
height_tiles = 40 height_tiles = 30
refund_percentage = 100 refund_percentage = 100
deconstruction_time_seconds = 0.1 deconstruction_time_seconds = 0.1
starting_building_blocks = 200 starting_building_blocks = 200
@@ -15,7 +15,7 @@ building_blocks_tooltip = "Building blocks are the currency for construction. Sp
artifact_tooltip = "Artifacts are the key to victory. Earn one by choosing the artifact reward when you destroy a set of enemy defence stations. Collect enough of them to win the game." artifact_tooltip = "Artifacts are the key to victory. Earn one by choosing the artifact reward when you destroy a set of enemy defence stations. Collect enough of them to win the game."
[regions] [regions]
asteroid_width_tiles = 60 asteroid_width_tiles = 40
player_buffer_width_tiles = 20 player_buffer_width_tiles = 20
contest_zone_width_tiles = 60 contest_zone_width_tiles = 60
enemy_buffer_width_tiles = 20 enemy_buffer_width_tiles = 20

View File

@@ -1,7 +1,5 @@
#include "SelectionPanel.h" #include "SelectionPanel.h"
#include <QLayout>
#include <QList>
#include <QScrollArea> #include <QScrollArea>
#include <QScrollBar> #include <QScrollBar>
#include <QVBoxLayout> #include <QVBoxLayout>
@@ -265,16 +263,9 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
// laid out, does the width it reports. Measuring at whatever width the panel // 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. // happens to have carries the previous card's shape into this one.
// //
// Each measurement re-runs the card's layouts -- every one of them, not just the // Each measurement re-runs the body layout. Cards are built and discarded whole, so
// body's own. Changing a label's text posts a LayoutRequest to the widget holding it // its cached hint describes the card before this one until it is invalidated, and
// and that event is only delivered when the event loop next runs, so a layout nested // re-running it is also what accounts for the parts a card hides and shows as it
// 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 // 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 // unstyled hint until the stylesheet has reached it, and the chips carry border and
// padding that change their size. // padding that change their size.
@@ -282,16 +273,6 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
{ {
m_body->resize(widthPx, m_body->height()); m_body->resize(widthPx, m_body->height());
m_body->ensurePolished(); 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()->invalidate();
m_body->layout()->activate(); m_body->layout()->activate();
return m_body->sizeHint(); return m_body->sizeHint();

View File

@@ -32,38 +32,6 @@ const int kSymbolSizePx = 20;
const int kCardSpacingPx = 6; const int kCardSpacingPx = 6;
const int kHeaderSpacingPx = 6; const int kHeaderSpacingPx = 6;
// The Salvage Bay has no recipe and no cycle: its two states say whether it is holding
// scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY, REQ-UI-SELECTION-STATUS).
QString getStatusCaption(ProductionStatus status, bool isSalvageBay)
{
switch (status)
{
case ProductionStatus::Unconfigured: return QObject::tr("no recipe");
case ProductionStatus::Producing:
return isSalvageBay ? QObject::tr("holding scrap")
: QObject::tr("producing");
case ProductionStatus::Starved:
return isSalvageBay ? QObject::tr("empty") : QObject::tr("missing input");
case ProductionStatus::Blocked: return QObject::tr("output full");
}
return QString();
}
// Every caption the status slot can end up showing for this building, so the header can
// keep its width as the state changes rather than the panel jumping with it.
QStringList getAllStatusCaptions(bool isSalvageBay)
{
QStringList captions;
for (ProductionStatus status : { ProductionStatus::Unconfigured,
ProductionStatus::Producing,
ProductionStatus::Starved,
ProductionStatus::Blocked })
{
captions << getStatusCaption(status, isSalvageBay);
}
return captions;
}
} // namespace } // namespace
@@ -200,26 +168,25 @@ void SelectionContent::setProductionStatusSlot(const Building& building)
return; return;
} }
const bool isSalvageBay = (building.type == BuildingType::SalvageBay);
// Room for every caption this building can show, so the panel keeps its width as the
// building's state changes rather than jumping with it.
reserveSlotFor(getAllStatusCaptions(isSalvageBay));
const StatusLightVisuals& colors = m_context.visuals->statusLight; const StatusLightVisuals& colors = m_context.visuals->statusLight;
QColor dotColor; // The Salvage Bay has no recipe and no cycle: its two states say whether it is
// holding scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY).
const bool isSalvageBay = (building.type == BuildingType::SalvageBay);
switch (*status) switch (*status)
{ {
case ProductionStatus::Unconfigured: dotColor = colors.grey; break; case ProductionStatus::Unconfigured:
case ProductionStatus::Producing: dotColor = colors.green; break; setSlot(colors.grey, tr("no recipe"));
case ProductionStatus::Starved: dotColor = colors.red; break; break;
case ProductionStatus::Blocked: dotColor = colors.yellow; break; case ProductionStatus::Producing:
setSlot(colors.green, isSalvageBay ? tr("holding scrap") : tr("producing"));
break;
case ProductionStatus::Starved:
setSlot(colors.red, isSalvageBay ? tr("empty") : tr("missing input"));
break;
case ProductionStatus::Blocked:
setSlot(colors.yellow, tr("output full"));
break;
} }
setSlot(dotColor, getStatusCaption(*status, isSalvageBay));
}
void SelectionContent::reserveSlotFor(const QStringList& captions)
{
m_statusPill->reserveFor(captions);
} }
void SelectionContent::refreshConstruction() void SelectionContent::refreshConstruction()

View File

@@ -5,7 +5,6 @@
#include <QColor> #include <QColor>
#include <QPixmap> #include <QPixmap>
#include <QString> #include <QString>
#include <QStringList>
#include <QWidget> #include <QWidget>
#include "BuildingId.h" #include "BuildingId.h"
@@ -80,11 +79,6 @@ protected:
void setCountSlot(int count); void setCountSlot(int count);
void clearSlot(); void clearSlot();
// Reserves header room for the widest caption the slot will ever show. The panel is
// sized to its card (REQ-UI-SELECTION-PANEL), so without this it changes width every
// time the caption does -- and a card whose chips wrap changes height with it.
void reserveSlotFor(const QStringList& captions);
// Fills the right slot from the building's production status, mapped to the same // Fills the right slot from the building's production status, mapped to the same
// colors and states the world's status light uses (REQ-UI-SELECTION-STATUS). Leaves // colors and states the world's status light uses (REQ-UI-SELECTION-STATUS). Leaves
// the slot empty for a type that has no status light. // the slot empty for a type that has no status light.

View File

@@ -33,16 +33,3 @@ QString getBehaviorLabel(BehaviorKind kind)
} }
return QString(); return QString();
} }
QStringList getAllBehaviorLabels()
{
QStringList labels;
for (BehaviorKind kind : { BehaviorKind::Retreat, BehaviorKind::Attack,
BehaviorKind::SalvageScrap, BehaviorKind::Repair,
BehaviorKind::Rally, BehaviorKind::Standby,
BehaviorKind::Advance })
{
labels << getBehaviorLabel(kind);
}
return labels;
}

View File

@@ -1,7 +1,6 @@
#pragma once #pragma once
#include <QString> #include <QString>
#include <QStringList>
#include "BehaviorKind.h" #include "BehaviorKind.h"
#include "BuildingType.h" #include "BuildingType.h"
@@ -14,8 +13,3 @@ QString getBuildingTypeName(BuildingType type);
// Name of the behavior currently governing a ship, for the ship card's header slot // Name of the behavior currently governing a ship, for the ship card's header slot
// (REQ-UI-SHIP-BEHAVIOR). Empty when no behavior has won yet, which shows no slot. // (REQ-UI-SHIP-BEHAVIOR). Empty when no behavior has won yet, which shows no slot.
QString getBehaviorLabel(BehaviorKind kind); QString getBehaviorLabel(BehaviorKind kind);
// Every name getBehaviorLabel can return. A ship's behavior changes as it fights, so the
// header reserves room for the widest of these rather than letting the panel change
// width each time (REQ-UI-SELECTION-PANEL).
QStringList getAllBehaviorLabels();

View File

@@ -21,10 +21,6 @@ ShipContent::ShipContent(const SelectionContext& context,
m_statsPanel = new ShipStatsPanel(context.config, this); m_statsPanel = new ShipStatsPanel(context.config, this);
getRuntimeLayout()->addWidget(m_statsPanel); getRuntimeLayout()->addWidget(m_statsPanel);
// A ship's behavior changes as it fights, so the header holds room for the longest
// name rather than the panel resizing under the player each time it does.
reserveSlotFor(getAllBehaviorLabels());
EntityAdmin& admin = context.sim->getAdmin(); EntityAdmin& admin = context.sim->getAdmin();
if (admin.isValid(m_entity) && admin.hasAll<ShipIdentityComponent>(m_entity)) if (admin.isValid(m_entity) && admin.hasAll<ShipIdentityComponent>(m_entity))
{ {

View File

@@ -1,6 +1,5 @@
#include "StatusPill.h" #include "StatusPill.h"
#include <QFontMetrics>
#include <QGuiApplication> #include <QGuiApplication>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
@@ -50,23 +49,6 @@ StatusPill::StatusPill(QWidget* parent)
hide(); hide();
} }
void StatusPill::reserveFor(const QStringList& captions)
{
if (captions == m_reservedFor)
{
return;
}
m_reservedFor = captions;
const QFontMetrics metrics(m_captionLabel->font());
int widestPx = 0;
for (const QString& caption : captions)
{
widestPx = qMax(widestPx, metrics.horizontalAdvance(caption));
}
m_captionLabel->setMinimumWidth(widestPx);
}
void StatusPill::setStatus(const QColor& dotColor, const QColor& outlineColor, void StatusPill::setStatus(const QColor& dotColor, const QColor& outlineColor,
const QString& caption) const QString& caption)
{ {

View File

@@ -2,7 +2,6 @@
#include <QColor> #include <QColor>
#include <QString> #include <QString>
#include <QStringList>
#include <QWidget> #include <QWidget>
class QLabel; class QLabel;
@@ -24,16 +23,7 @@ public:
void setStatus(const QColor& dotColor, const QColor& outlineColor, void setStatus(const QColor& dotColor, const QColor& outlineColor,
const QString& caption); const QString& caption);
// Reserves room for the widest of the captions this pill can show, so the header --
// and with it the panel, which is sized to its content (REQ-UI-SELECTION-PANEL) --
// keeps its width as the caption changes. Without it the whole panel jumps every
// time a building's status does, and a card whose chips wrap changes height with it.
void reserveFor(const QStringList& captions);
private: private:
QLabel* m_dotLabel; QLabel* m_dotLabel;
QLabel* m_captionLabel; QLabel* m_captionLabel;
// What the width is currently reserved for, so re-reserving the same set on every
// refresh costs nothing.
QStringList m_reservedFor;
}; };