From abe8c8f448d55ae733d8f147d1546306485cd553 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Tue, 18 Aug 2026 22:55:06 +0200 Subject: [PATCH] stand the expansion button on the ground it buys The button leaves the header bar for the world, centered on the columns the next purchase unlocks and panning with them until it leaves the view. Those columns now take a lighter tint than the locked ground behind them, so the boundary between the two says how far one purchase reaches. Nothing in the code reported a scroll: the camera knew it had moved and told only the hover. A ViewScrolledEvent says it now, which is what a widget keeping a place in the world rather than on the screen needs. The panels want none of it -- they are placed against the screen and stay put as the world moves under them -- so the button is placed after their pass and never joins the rectangles they step around. Its face is two lines, "Expand" over the cost. That makes the cost a display of its own, so it carries the building-block tooltip the way every other item value does, on hover alone since the click buys the expansion. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x --- bin/app/data/config/visuals.toml | 1 + src/lib/eventsystem/event/CMakeLists.txt | 1 + .../event/ExpansionCostChangedEvent.h | 5 +- src/lib/eventsystem/event/ViewScrolledEvent.h | 15 +++ src/ui/CMakeLists.txt | 2 + src/ui/ExpandButton.cpp | 99 +++++++++++++++++++ src/ui/ExpandButton.h | 44 +++++++++ src/ui/GameWorldView.cpp | 10 ++ src/ui/GameWorldView.h | 12 ++- src/ui/HeaderBar.cpp | 57 ----------- src/ui/HeaderBar.h | 16 +-- src/ui/MainWindow.cpp | 57 +++++++++++ src/ui/MainWindow.h | 13 ++- src/ui/VisualsConfig.h | 4 + src/ui/VisualsLoader.cpp | 1 + src/ui/WorldRenderer.cpp | 14 ++- 16 files changed, 271 insertions(+), 80 deletions(-) create mode 100644 src/lib/eventsystem/event/ViewScrolledEvent.h create mode 100644 src/ui/ExpandButton.cpp create mode 100644 src/ui/ExpandButton.h diff --git a/bin/app/data/config/visuals.toml b/bin/app/data/config/visuals.toml index b22afaf..1581374 100644 --- a/bin/app/data/config/visuals.toml +++ b/bin/app/data/config/visuals.toml @@ -352,6 +352,7 @@ selected_outline = "#ffff00" # outline around currently-selected building(s), # box-drag selection rectangle (REQ-UI-MULTI-SELECT) config_transfer = "#33ccff66" # blueprint ghost over a configuration-transfer target (REQ-UI-BLUEPRINT-TRANSFER) locked_asteroid = "#0000007f" # tint over the asteroid left of the buildable edge (not yet unlocked by expansion) +next_expansion = "#00000040" # lighter tint over the columns the next expansion unlocks (REQ-UI-LOCKED-ASTEROID) modal_dim = "#00000099" # semi-transparent black dim behind modal dialogs/menus (REQ-UI-MODAL-DIM) tunnel_preview = "#00ff0055" # tunnel connection preview: matched end + tiles between (REQ-BLD-TUNNEL-MODE) diff --git a/src/lib/eventsystem/event/CMakeLists.txt b/src/lib/eventsystem/event/CMakeLists.txt index 349c550..6c3051e 100644 --- a/src/lib/eventsystem/event/CMakeLists.txt +++ b/src/lib/eventsystem/event/CMakeLists.txt @@ -47,6 +47,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/CommandRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/FloatingLayoutInvalidatedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/PlayerCommandsAppliedEvent.h + ${CMAKE_CURRENT_SOURCE_DIR}/ViewScrolledEvent.h PARENT_SCOPE ) diff --git a/src/lib/eventsystem/event/ExpansionCostChangedEvent.h b/src/lib/eventsystem/event/ExpansionCostChangedEvent.h index 682e90e..1897aa1 100644 --- a/src/lib/eventsystem/event/ExpansionCostChangedEvent.h +++ b/src/lib/eventsystem/event/ExpansionCostChangedEvent.h @@ -5,8 +5,9 @@ // Fired when the current asteroid-expansion cost changes (REQ-EXP-COST): once at // startup and again after each expansion is purchased. Carries no payload — the -// header Expand button re-reads Simulation::getCurrentExpansionCost() to update -// its caption and enabled state (REQ-UI-EXPAND-BUTTON). +// expansion button re-reads Simulation::getCurrentExpansionCost() to update its cost +// line and enabled state, and moves onto the next stretch of locked ground, the +// buildable edge having shifted with the purchase (REQ-UI-EXPAND-BUTTON). class ExpansionCostChangedEvent : public Event { }; diff --git a/src/lib/eventsystem/event/ViewScrolledEvent.h b/src/lib/eventsystem/event/ViewScrolledEvent.h new file mode 100644 index 0000000..19beb0e --- /dev/null +++ b/src/lib/eventsystem/event/ViewScrolledEvent.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Event.h" + +// Fired when the game world view's scroll position actually moved (REQ-UI-SCROLL), once +// per frame at most and not at all while the view sits still. Carries no payload: what it +// says is that the world-to-widget transform has changed, and whoever cares re-reads it. +// +// It exists for the widgets that keep a place in the world rather than on the screen -- +// the asteroid expansion button, which stands on the columns it would buy +// (REQ-UI-EXPAND-BUTTON). The floating panels need nothing of the sort: they are placed +// against the screen and stay put as the view moves under them (ui/FloatingPanel.h). +class ViewScrolledEvent : public Event +{ +}; diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 5ab66b5..d14cbe1 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -17,6 +17,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h ${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanel.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h + ${CMAKE_CURRENT_SOURCE_DIR}/ExpandButton.h ${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h ${CMAKE_CURRENT_SOURCE_DIR}/SelectionBounds.h ${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h @@ -60,6 +61,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ExpandButton.cpp ${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/SelectionBounds.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.cpp diff --git a/src/ui/ExpandButton.cpp b/src/ui/ExpandButton.cpp new file mode 100644 index 0000000..fcb7114 --- /dev/null +++ b/src/ui/ExpandButton.cpp @@ -0,0 +1,99 @@ +#include "ExpandButton.h" + +#include +#include +#include +#include + +#include "Command.h" +#include "CommandRequestedEvent.h" +#include "EventManager.h" +#include "FloatingLayoutInvalidatedEvent.h" +#include "IconCaption.h" +#include "ItemIconCache.h" +#include "ItemTooltip.h" +#include "Simulation.h" +#include "TooltipTrigger.h" + +ExpandButton::ExpandButton(const ItemTooltipContext& context, QWidget* parent) + : OptionButton(parent) + , m_context(context) +{ + QVBoxLayout* face = new QVBoxLayout(this); + face->setContentsMargins(8, 4, 8, 4); + face->setSpacing(1); + + // Names the action, not an item, so it says nothing of its own and stays out of the + // mouse's way -- a click anywhere on the face reaches the button beneath it. + QLabel* actionLabel = new QLabel(tr("Expand"), this); + actionLabel->setAlignment(Qt::AlignCenter); + actionLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true); + face->addWidget(actionLabel); + + // The cost line, deliberately not transparent to the mouse: it names an item and has + // to be hoverable to explain it. Hover only, the click being the button's + // (REQ-UI-ITEM-VALUE-TOOLTIP). A label ignores presses, so they still reach the + // button. + m_costLabel = new QLabel(this); + m_costLabel->setAlignment(Qt::AlignCenter); + face->addWidget(m_costLabel); + ItemTooltip::attachTo(*m_costLabel, m_context, kBlockItemId, + TooltipTrigger::Trigger::HoverOnly); + + // Carries no payload: the simulation derives the cost and the column count from its + // own expansion counter (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND). + connect(this, &QPushButton::clicked, this, []() { + EventManager::getInstance()->sendEventImmediately( + std::make_shared( + std::make_shared())); + }); + + refresh(); + registerForEvents(); +} + +ExpandButton::~ExpandButton() +{ + unregisterForEvents(); +} + +void ExpandButton::handleEvent(std::shared_ptr /*event*/) +{ + refresh(); +} + +void ExpandButton::handleEvent(std::shared_ptr /*event*/) +{ + refresh(); +} + +void ExpandButton::refresh() +{ + const int blocks = m_context.sim->getBuildingBlocksStock(); + const int expansionCost = m_context.sim->getCurrentExpansionCost(); + + setEnabled(blocks >= expansionCost); + + // The cost reads as it does everywhere else: the number, then the block icon in place + // of a trailing `Blocks` word (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON). With no icon + // file the word comes back, since nothing else on the line would name the item. + const QPixmap icon = m_context.itemIcons->getInlineIcon(kBlockItemId, font()); + if (icon.isNull()) + { + m_costLabel->setPixmap(QPixmap()); + m_costLabel->setText(tr("%1 Blocks").arg(expansionCost)); + } + else + { + m_costLabel->setText(QString()); + m_costLabel->setPixmap(renderCaptionWithIcon( + QString::number(expansionCost), icon, font(), + palette().color(isEnabled() ? QPalette::Active : QPalette::Disabled, + QPalette::ButtonText))); + } + + // A cost of a different width re-centers the button on its columns, which is the + // owner's to do (ui/FloatingPanel.h, MainWindow::placeExpandButton). + EventManager::getInstance()->sendEventImmediately( + std::make_shared()); +} diff --git a/src/ui/ExpandButton.h b/src/ui/ExpandButton.h new file mode 100644 index 0000000..e533475 --- /dev/null +++ b/src/ui/ExpandButton.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include "BuildingBlocksChangedEvent.h" +#include "EventHandler.h" +#include "ExpansionCostChangedEvent.h" +#include "ItemTooltipContext.h" +#include "OptionButton.h" + +class QLabel; + +// The asteroid expansion button (REQ-UI-EXPAND-BUTTON): a button that stands in the game +// world on the columns the next purchase would unlock, rather than in the header bar. +// +// It knows what it says and what it costs; where it stands is MainWindow's to decide, the +// position being a place in the world and this button knowing nothing of the view's scroll +// (MainWindow::placeExpandButton). +// +// Its face is two lines, `Expand` over the cost, so that the cost is a display naming an +// item and can explain it the way every other item value does -- on hover only, the click +// belonging to the button (REQ-UI-ITEM-VALUE-TOOLTIP). +class ExpandButton : public OptionButton, + public CombinedEventHandler +{ + Q_OBJECT + +public: + // Nothing in the context is owned; all of it must outlive this widget. + explicit ExpandButton(const ItemTooltipContext& context, QWidget* parent = nullptr); + ~ExpandButton() override; + +private: + void handleEvent(std::shared_ptr event) override; + void handleEvent(std::shared_ptr event) override; + + // Re-states the cost and whether the player can pay it. Asks for a fresh placement + // pass afterwards, because a wider cost re-centers the button. + void refresh(); + + ItemTooltipContext m_context; + QLabel* m_costLabel; +}; diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 4a17101..8ec6cd2 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -68,6 +68,7 @@ #include "BuildingBlocksChangedEvent.h" #include "UnlockedBuildingsChangedEvent.h" #include "ExpansionCostChangedEvent.h" +#include "ViewScrolledEvent.h" #include "GameSpeedChangedEvent.h" #include "SchematicChoicesAvailableEvent.h" #include "PlayerCommandsAppliedEvent.h" @@ -302,6 +303,15 @@ void GameWorldView::onFrame() { refreshHover(); } + + // The world-to-widget transform has changed, which no other event reports. What + // stands in the world rather than on the screen re-places itself on this + // (REQ-UI-EXPAND-BUTTON). + if (viewMoved) + { + EventManager::getInstance()->sendEventImmediately( + std::make_shared()); + } } // Fire events for any state that changed since the last frame diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index c08275e..679699f 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -120,6 +120,13 @@ public: // no blueprint is active and for the unnamed temporary one (REQ-UI-BLUEPRINT-TEMP). QString getActiveBlueprintName() const; + // The world <-> widget transform for the current viewport size and scroll + // position. Cheap to build and deliberately not cached: it is a snapshot that + // a resize or a scroll invalidates, so every user takes a fresh one. Public because + // a widget standing in the world needs it to find its place (REQ-UI-EXPAND-BUTTON); + // a ViewScrolledEvent says when it has gone stale. + WorldCoordinates getCoordinates() const; + protected: void initializeGL() override; void paintGL() override; @@ -189,11 +196,6 @@ private: // Gathers the interaction state the renderer needs for this frame. WorldRenderFrame makeRenderFrame() const; - // The world <-> widget transform for the current viewport size and scroll - // position. Cheap to build and deliberately not cached: it is a snapshot that - // a resize or a scroll invalidates, so every user takes a fresh one. - WorldCoordinates getCoordinates() const; - float getAsteroidLeftEdge() const; float getEnemyStationRightEdge() const; // The camera's current pan limits, read fresh from the simulation each frame: diff --git a/src/ui/HeaderBar.cpp b/src/ui/HeaderBar.cpp index 1862593..77322d9 100644 --- a/src/ui/HeaderBar.cpp +++ b/src/ui/HeaderBar.cpp @@ -4,15 +4,11 @@ #include #include -#include #include #include #include #include -#include -#include "Command.h" -#include "CommandRequestedEvent.h" #include "EventManager.h" #include "IconCaption.h" #include "ItemIconCache.h" @@ -56,16 +52,6 @@ HeaderBar::HeaderBar(const ItemTooltipContext& context, QWidget* parent) m_bossWaveLabel = new QLabel(tr("Boss Wave #1"), this); m_nextBossLabel = new QLabel(tr("Next boss: 5:00"), this); - // Asteroid expansion button, to the left of the speed buttons (REQ-UI-HEADER, - // REQ-UI-EXPAND-BUTTON). Caption/enabled state are set on the first - // ExpansionCostChangedEvent; clicking requests an ExpandAsteroidCommand. - m_expandButton = new QPushButton(tr("Expand"), this); - connect(m_expandButton, &QPushButton::clicked, this, []() { - EventManager::getInstance()->sendEventImmediately( - std::make_shared( - std::make_shared())); - }); - const int itemSpacing = 20; layout->addWidget(m_timeLabel); @@ -78,8 +64,6 @@ HeaderBar::HeaderBar(const ItemTooltipContext& context, QWidget* parent) layout->addSpacing(itemSpacing); layout->addWidget(m_nextBossLabel); layout->addSpacing(itemSpacing); - layout->addWidget(m_expandButton); - layout->addSpacing(itemSpacing); const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" }; QSignalMapper* mapper = new QSignalMapper(this); @@ -117,12 +101,6 @@ void HeaderBar::handleEvent(std::shared_ptr /*event*/) void HeaderBar::handleEvent(std::shared_ptr /*event*/) { updateBlocksLabel(); - updateExpandButton(); -} - -void HeaderBar::handleEvent(std::shared_ptr /*event*/) -{ - updateExpandButton(); } void HeaderBar::updateBlocksLabel() @@ -141,41 +119,6 @@ void HeaderBar::updateBlocksLabel() m_blocksLabel->palette().color(QPalette::WindowText))); } -void HeaderBar::updateExpandButton() -{ - const int blocks = m_context.sim->getBuildingBlocksStock(); - const int expansionCost = m_context.sim->getCurrentExpansionCost(); - - m_expandButton->setEnabled(blocks >= expansionCost); - - const QPixmap icon = m_context.itemIcons->getInlineIcon(kBlockItemId, font()); - if (icon.isNull()) - { - // Fallback text form when no building_block icon exists (REQ-UI-EXPAND-BUTTON). - m_expandButton->setIcon(QIcon()); - m_expandButton->setText(tr("Expand: %1 Blocks").arg(expansionCost)); - return; - } - - const QString text = tr("Expand: %1").arg(expansionCost); - const QPalette& pal = m_expandButton->palette(); - const QPixmap normal = renderCaptionWithIcon( - text, icon, m_expandButton->font(), pal.color(QPalette::ButtonText)); - const QPixmap greyed = renderCaptionWithIcon( - text, icon, m_expandButton->font(), - pal.color(QPalette::Disabled, QPalette::ButtonText)); - - QIcon buttonIcon; - buttonIcon.addPixmap(normal, QIcon::Normal); - buttonIcon.addPixmap(greyed, QIcon::Disabled); - m_expandButton->setText(QString()); - m_expandButton->setIcon(buttonIcon); - const qreal dpr = normal.devicePixelRatio(); - m_expandButton->setIconSize(QSize( - static_cast(normal.width() / dpr), - static_cast(normal.height() / dpr))); -} - void HeaderBar::handleEvent(std::shared_ptr event) { for (int i = 0; i < kSpeedCount; ++i) diff --git a/src/ui/HeaderBar.h b/src/ui/HeaderBar.h index 108a369..ee4e58e 100644 --- a/src/ui/HeaderBar.h +++ b/src/ui/HeaderBar.h @@ -11,7 +11,6 @@ #include "BossWaveUpdatedEvent.h" #include "BuildingBlocksChangedEvent.h" #include "EventHandler.h" -#include "ExpansionCostChangedEvent.h" #include "GameConfig.h" #include "GameSpeedChangedEvent.h" #include "ItemTooltipContext.h" @@ -26,7 +25,6 @@ class Simulation; class HeaderBar : public QWidget, public CombinedEventHandler @@ -35,10 +33,10 @@ class HeaderBar : public QWidget, public: // The context carries the simulation the bar reads, the config, and the icon caches - // -- the item cache draws the building_block icon in the stock display and the expand - // button (REQ-UI-BLOCKS-ICON, REQ-UI-EXPAND-BUTTON), and the stock display explains - // that item with the cache's help (REQ-UI-ITEM-VALUE-TOOLTIP). Nothing in it is - // owned; all of it must outlive this widget. + // -- the item cache draws the building_block icon in the stock display + // (REQ-UI-BLOCKS-ICON), and the stock display explains that item with the cache's + // help (REQ-UI-ITEM-VALUE-TOOLTIP). Nothing in it is owned; all of it must outlive + // this widget. explicit HeaderBar(const ItemTooltipContext& context, QWidget* parent = nullptr); ~HeaderBar() override; @@ -48,15 +46,10 @@ private slots: private: void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; - void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; - // Refreshes the Expand button caption and enabled state from the current - // expansion cost and building block stock (REQ-UI-EXPAND-BUTTON). - void updateExpandButton(); - // Refreshes the building blocks stock display from the simulation: // `Stock: ` with the building_block icon after it, or the // `Stock: Blocks` text fallback when no icon file exists @@ -68,7 +61,6 @@ private: QLabel* m_artifactsLabel; QLabel* m_bossWaveLabel; QLabel* m_nextBossLabel; - QPushButton* m_expandButton; std::vector m_speedButtons; // Not owned; all of it lives in MainWindow. The simulation it names is the single diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index db1ddf5..e5ee3b4 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -11,8 +11,10 @@ #include #include #include +#include #include #include +#include #include "BlueprintLibrary.h" #include "BlueprintSelectionDialog.h" @@ -28,6 +30,7 @@ #include "HeaderBar.h" #include "SelectionPanel.h" #include "ControlsPanel.h" +#include "ExpandButton.h" #include "ShipLayoutBlueprintSerializer.h" #include "ShipLayoutDialog.h" #include "BuildingIconCache.h" @@ -74,6 +77,13 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, m_buildingIcons.get(), m_itemIcons.get(), this); + // Stands in the world rather than on the screen, on the columns the next expansion + // unlocks (REQ-UI-EXPAND-BUTTON). A sibling of the world view like the panels, which + // is what keeps the builder-mode ghost off the tile beneath it and the click off the + // world (REQ-BLD-GHOST): the view's hover follows underMouse(), false while the + // cursor rests on a sibling. + m_expandButton = new ExpandButton(getItemTooltipContext(), this); + // The blueprints have no widget of their own: they are saved with Ctrl+C and picked // from a modal dialog (REQ-UI-BLUEPRINT-DIALOG), both driven from this window // because only it can pause the game and raise the dim overlay. Built after the @@ -223,6 +233,11 @@ void MainWindow::layoutPanels() } } + // Last, and never added to occupiedRects: it marks a place in the world, so the + // panels do not step around it and it does not step around them + // (REQ-UI-EXPAND-BUTTON). + placeExpandButton(); + m_layingOut = false; } @@ -494,6 +509,48 @@ void MainWindow::handleEvent(std::shared_ptrgetCurrentAsteroidWidth_tiles(); + const float columns_tiles = static_cast( + m_sim->getConfig().world.expansion.columnsPerExpansion_tiles); + const QVector2D center_tiles( + -static_cast(asteroidWidth_tiles) - columns_tiles / 2.0f, + static_cast(m_sim->getConfig().world.heightTiles) / 2.0f); + + // The transform is the view's, so the point it yields is in the view's coordinates; + // the button is a sibling of the view, so it is lifted into this window's. + const QPointF centerInView_px = + m_gameWorldView->getCoordinates().worldToWidget(center_tiles); + const QPoint center_px = m_gameWorldView->geometry().topLeft() + + QPoint(static_cast(centerInView_px.x()), + static_cast(centerInView_px.y())); + + // Neither clamped nor hidden: the button belongs to that ground and leaves the view + // with it, which the window's own edge takes care of (REQ-UI-EXPAND-BUTTON). + const QSize buttonSize = m_expandButton->sizeHint(); + m_expandButton->setGeometry(QRect( + center_px - QPoint(buttonSize.width() / 2, buttonSize.height() / 2), + buttonSize)); +} + +void MainWindow::handleEvent(std::shared_ptr /*event*/) +{ + // The ground moved under the button; nothing else about the layout changed, so this + // is the one widget to re-place (REQ-UI-EXPAND-BUTTON). + placeExpandButton(); +} + ItemTooltipContext MainWindow::getItemTooltipContext() const { return ItemTooltipContext{ m_sim, &m_sim->getConfig(), m_itemIcons.get(), diff --git a/src/ui/MainWindow.h b/src/ui/MainWindow.h index c2d7ebe..348f70c 100644 --- a/src/ui/MainWindow.h +++ b/src/ui/MainWindow.h @@ -22,6 +22,7 @@ #include "RecipeSelectionRequestedEvent.h" #include "SchematicChoicesAvailableEvent.h" #include "ShipLayout.h" +#include "ViewScrolledEvent.h" #include "ShipLayoutBlueprint.h" #include "Tick.h" #include "VisualsConfig.h" @@ -35,6 +36,7 @@ class ControlsPanel; class BuildButtonBar; class BlueprintLibrary; class BuildingIconCache; +class ExpandButton; class ItemIconCache; class QCloseEvent; class QResizeEvent; @@ -48,7 +50,8 @@ class MainWindow : public QWidget, RecipeSelectionRequestedEvent, BlueprintSaveRequestedEvent, BlueprintSelectionRequestedEvent, - FloatingLayoutInvalidatedEvent> + FloatingLayoutInvalidatedEvent, + ViewScrolledEvent> { Q_OBJECT @@ -71,6 +74,7 @@ private: void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; + void handleEvent(std::shared_ptr event) override; // Reloads the game config and visuals.toml from disk (REQ-CFG-RELOAD), shared // by every restart path. On success the reloaded visuals are applied to this @@ -97,6 +101,10 @@ private: // What every display naming an item needs to explain it (REQ-UI-ITEM-VALUE-TOOLTIP). // Assembled here because this is where the simulation and the icon caches live. ItemTooltipContext getItemTooltipContext() const; + // Centers the expansion button on the columns the next purchase unlocks + // (REQ-UI-EXPAND-BUTTON). Its place is in the world, so it is re-derived whenever the + // view scrolls or resizes rather than once per selection like the panels. + void placeExpandButton(); // Places the widgets floating over the game world view, in one ordered pass // (FloatingPanel.h). Runs on a resize and on every FloatingLayoutInvalidatedEvent. void layoutPanels(); @@ -116,6 +124,9 @@ private: SelectionPanel* m_selectionPanel; ControlsPanel* m_controlsPanel; BuildButtonBar* m_buildButtonBar; + // Stands in the world on the ground it would buy, so it takes no part in the panels' + // placement pass (REQ-UI-EXPAND-BUTTON). + ExpandButton* m_expandButton = nullptr; // The saved blueprints themselves; they have no widget of their own any more and // are reached through the two modal dialogs (REQ-UI-BLUEPRINT-DIALOG). std::unique_ptr m_blueprintLibrary; diff --git a/src/ui/VisualsConfig.h b/src/ui/VisualsConfig.h index 106319c..d464742 100644 --- a/src/ui/VisualsConfig.h +++ b/src/ui/VisualsConfig.h @@ -49,6 +49,10 @@ struct OverlayVisuals QColor selectedOutline; QColor configTransfer; // blueprint ghost over a transfer target (REQ-UI-BLUEPRINT-TRANSFER) QColor lockedAsteroid; + // Tint over the columns the next expansion unlocks, lighter than lockedAsteroid so + // the ground one purchase away reads apart from the ground behind it + // (REQ-UI-LOCKED-ASTEROID). + QColor nextExpansion; QColor modalDim; QColor tunnelPreview; // tunnel connection preview highlight (REQ-BLD-TUNNEL-MODE) }; diff --git a/src/ui/VisualsLoader.cpp b/src/ui/VisualsLoader.cpp index 0156d6a..eb766ca 100644 --- a/src/ui/VisualsLoader.cpp +++ b/src/ui/VisualsLoader.cpp @@ -225,6 +225,7 @@ VisualsConfig VisualsLoader::load(const std::string& path) cfg.overlays.selectedOutline = parseColor(requireString(ov, "selected_outline", "overlays"), "overlays.selected_outline"); cfg.overlays.configTransfer = parseColor(requireString(ov, "config_transfer", "overlays"), "overlays.config_transfer"); cfg.overlays.lockedAsteroid = parseColor(requireString(ov, "locked_asteroid", "overlays"), "overlays.locked_asteroid"); + cfg.overlays.nextExpansion = parseColor(requireString(ov, "next_expansion", "overlays"), "overlays.next_expansion"); cfg.overlays.modalDim = parseColor(requireString(ov, "modal_dim", "overlays"), "overlays.modal_dim"); cfg.overlays.tunnelPreview = parseColor(requireString(ov, "tunnel_preview", "overlays"), "overlays.tunnel_preview"); } diff --git a/src/ui/WorldRenderer.cpp b/src/ui/WorldRenderer.cpp index af80488..50fec8a 100644 --- a/src/ui/WorldRenderer.cpp +++ b/src/ui/WorldRenderer.cpp @@ -203,9 +203,14 @@ void WorldRenderer::drawTiles(QPainter& painter, const WorldCoordinates& coordin + static_cast(std::ceil(coordinates.getViewportWidthTiles())) + 2; const int bottomTile = m_sim.getConfig().world.heightTiles; - // Asteroid columns left of the buildable edge are not yet unlocked by - // expansion; tint them so the player sees the reachable-but-locked area. + // Asteroid columns left of the buildable edge are not yet unlocked by expansion; + // tint them so the player sees the reachable-but-locked area. The columns one + // purchase away take a lighter tint than the ground behind them, so the boundary + // between the two says how far the next expansion reaches, and the expansion button + // stands in the middle of them (REQ-UI-LOCKED-ASTEROID, REQ-UI-EXPAND-BUTTON). const int buildableLeftX = -m_sim.getCurrentAsteroidWidth_tiles(); + const int nextExpansionLeftX = + buildableLeftX - m_sim.getConfig().world.expansion.columnsPerExpansion_tiles; painter.setPen(Qt::NoPen); for (int x = leftTile; x <= rightTile; ++x) @@ -214,13 +219,16 @@ void WorldRenderer::drawTiles(QPainter& painter, const WorldCoordinates& coordin ? m_visuals.asteroid.fill : m_visuals.space.fill; const bool locked = (x < buildableLeftX); + const QColor& tint = (x >= nextExpansionLeftX) + ? m_visuals.overlays.nextExpansion + : m_visuals.overlays.lockedAsteroid; for (int y = 0; y < bottomTile; ++y) { const QRectF rect = coordinates.tileRect(QPoint(x, y)); painter.fillRect(rect, fill); if (locked) { - painter.fillRect(rect, m_visuals.overlays.lockedAsteroid); + painter.fillRect(rect, tint); } } }