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); } } }