From 51c6a4a56736eecffb3ee82a4d666d306b346c80 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Thu, 6 Aug 2026 15:11:49 +0200 Subject: [PATCH] bind the temporary blueprint to C and V Follows f9c654b, which rekeyed REQ-UI-BLUEPRINT-TEMP: C captures a temporary blueprint from the selection and enters its placement mode, V re-enters that mode later without capturing anything, and T is unbound. The plain keys are now the throwaway counterparts of Ctrl+C / Ctrl+V. V forces the blueprint to outlive its placement mode, so BlueprintModeExitedEvent no longer drops it -- it now clears only the saved-blueprint index. BlueprintLibrary holds the one temporary blueprint in an optional beside the saved list, which keeps it out of the selection dialog and out of blueprints.toml by construction. Restarting must discard it, and nothing announced a restart. GameResetEvent does, emitted from GameWorldView::resetForNewGame(): the escape menu, game over and win dialogs only enqueue a ResetCommand, so that is the single point where a restart has actually been applied. The alternative, calling into the library from all three dialogs, would triplicate the call and couple MainWindow to the library's internals. TemporaryBlueprintRequestedEvent is renamed to TemporaryBlueprintCaptureRequestedEvent -- with two temp-blueprint requests the old name no longer says which. Also drops a dead include of it from GameWorldView, unused since the library was split out of BlueprintPanel. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K --- src/lib/eventsystem/event/CMakeLists.txt | 4 ++- src/lib/eventsystem/event/GameResetEvent.h | 13 +++++++ .../TemporaryBlueprintCaptureRequestedEvent.h | 9 +++++ .../TemporaryBlueprintPlaceRequestedEvent.h | 10 ++++++ .../event/TemporaryBlueprintRequestedEvent.h | 9 ----- src/ui/BlueprintLibrary.cpp | 36 ++++++++++++++++--- src/ui/BlueprintLibrary.h | 25 ++++++++++--- src/ui/GameWorldView.cpp | 6 +++- src/ui/InputMapper.cpp | 22 ++++++++---- 9 files changed, 106 insertions(+), 28 deletions(-) create mode 100644 src/lib/eventsystem/event/GameResetEvent.h create mode 100644 src/lib/eventsystem/event/TemporaryBlueprintCaptureRequestedEvent.h create mode 100644 src/lib/eventsystem/event/TemporaryBlueprintPlaceRequestedEvent.h delete mode 100644 src/lib/eventsystem/event/TemporaryBlueprintRequestedEvent.h diff --git a/src/lib/eventsystem/event/CMakeLists.txt b/src/lib/eventsystem/event/CMakeLists.txt index 0405e37..a42245d 100644 --- a/src/lib/eventsystem/event/CMakeLists.txt +++ b/src/lib/eventsystem/event/CMakeLists.txt @@ -10,6 +10,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h + ${CMAKE_CURRENT_SOURCE_DIR}/GameResetEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/ArtifactCountChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/UnlockedBuildingsChangedEvent.h @@ -31,7 +32,8 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/DeconstructModeToggleRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/ExitBlueprintModeRequestedEvent.h - ${CMAKE_CURRENT_SOURCE_DIR}/TemporaryBlueprintRequestedEvent.h + ${CMAKE_CURRENT_SOURCE_DIR}/TemporaryBlueprintCaptureRequestedEvent.h + ${CMAKE_CURRENT_SOURCE_DIR}/TemporaryBlueprintPlaceRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/SpeedChangeRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/LayoutDialogRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionRequestedEvent.h diff --git a/src/lib/eventsystem/event/GameResetEvent.h b/src/lib/eventsystem/event/GameResetEvent.h new file mode 100644 index 0000000..e14e059 --- /dev/null +++ b/src/lib/eventsystem/event/GameResetEvent.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Event.h" + +// Emitted once per restart, after a queued ResetCommand has been applied and the view has +// been reset for the new run (REQ-UI-GAME-MENU). It lets presentation state that does not +// live in the simulation -- e.g. the temporary blueprint, REQ-UI-BLUEPRINT-TEMP -- be +// dropped for the fresh run. Sent from GameWorldView::resetForNewGame(), which is where a +// restart actually lands: the escape menu, game over, and win dialogs only enqueue the +// command. +class GameResetEvent : public Event +{ +}; diff --git a/src/lib/eventsystem/event/TemporaryBlueprintCaptureRequestedEvent.h b/src/lib/eventsystem/event/TemporaryBlueprintCaptureRequestedEvent.h new file mode 100644 index 0000000..2d717ed --- /dev/null +++ b/src/lib/eventsystem/event/TemporaryBlueprintCaptureRequestedEvent.h @@ -0,0 +1,9 @@ +#pragma once + +#include "Event.h" + +// Emitted when the player presses C to capture a temporary blueprint (REQ-UI-BLUEPRINT-TEMP). +// Carries no payload: the blueprint is built from the current selection by the receiver. +class TemporaryBlueprintCaptureRequestedEvent : public Event +{ +}; diff --git a/src/lib/eventsystem/event/TemporaryBlueprintPlaceRequestedEvent.h b/src/lib/eventsystem/event/TemporaryBlueprintPlaceRequestedEvent.h new file mode 100644 index 0000000..4e034d3 --- /dev/null +++ b/src/lib/eventsystem/event/TemporaryBlueprintPlaceRequestedEvent.h @@ -0,0 +1,10 @@ +#pragma once + +#include "Event.h" + +// Emitted when the player presses V to re-enter placement mode for the temporary blueprint +// captured with C (REQ-UI-BLUEPRINT-TEMP). Carries no payload: the receiver holds the +// blueprint, and nothing is captured from the current selection. +class TemporaryBlueprintPlaceRequestedEvent : public Event +{ +}; diff --git a/src/lib/eventsystem/event/TemporaryBlueprintRequestedEvent.h b/src/lib/eventsystem/event/TemporaryBlueprintRequestedEvent.h deleted file mode 100644 index 4c89ce5..0000000 --- a/src/lib/eventsystem/event/TemporaryBlueprintRequestedEvent.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "Event.h" - -// Emitted when the player presses the temporary-blueprint hotkey (REQ-UI-BLUEPRINT-TEMP). -// Carries no payload: the blueprint is built from the current selection by the receiver. -class TemporaryBlueprintRequestedEvent : public Event -{ -}; diff --git a/src/ui/BlueprintLibrary.cpp b/src/ui/BlueprintLibrary.cpp index 13177e1..2eb8b97 100644 --- a/src/ui/BlueprintLibrary.cpp +++ b/src/ui/BlueprintLibrary.cpp @@ -116,22 +116,48 @@ void BlueprintLibrary::handleEvent(std::shared_ptr void BlueprintLibrary::handleEvent(std::shared_ptr /*event*/) { + // Only the saved-blueprint index is cleared. A temporary blueprint deliberately + // survives its placement mode so V can re-enter it (REQ-UI-BLUEPRINT-TEMP). m_activeIndex = std::nullopt; } void BlueprintLibrary::handleEvent( - std::shared_ptr /*event*/) + std::shared_ptr /*event*/) { - // Temporary blueprint (REQ-UI-BLUEPRINT-TEMP): build from the current selection and - // enter placement mode without adding it to the list or persisting it. If nothing - // player-placeable is selected, do nothing. + // C (REQ-UI-BLUEPRINT-TEMP): capture the current selection and enter placement mode + // for it, without naming it, listing it, or persisting it. If nothing player-placeable + // is selected, do nothing at all -- in particular, keep the previous temporary + // blueprint, which V can still place. Blueprint blueprint = createBlueprintFromSelection(); if (blueprint.buildings.empty()) { return; } + m_temporaryBlueprint = std::move(blueprint); // No saved blueprint is active while a temporary one is being placed. m_activeIndex = std::nullopt; EventManager::getInstance()->sendEventImmediately( - std::make_shared(std::move(blueprint))); + std::make_shared(*m_temporaryBlueprint)); +} + +void BlueprintLibrary::handleEvent( + std::shared_ptr /*event*/) +{ + // V (REQ-UI-BLUEPRINT-TEMP): re-enter placement mode for the temporary blueprint, + // capturing nothing. With none captured, nothing happens -- no mode is entered and + // whichever mode is active is left alone. Affordability is not checked here, matching + // C; cost is enforced at placement (REQ-UI-BLUEPRINT-PLACE). The blueprint is copied, + // not moved: it stays available for the next V. + if (!m_temporaryBlueprint.has_value()) { return; } + + m_activeIndex = std::nullopt; + EventManager::getInstance()->sendEventImmediately( + std::make_shared(*m_temporaryBlueprint)); +} + +void BlueprintLibrary::handleEvent(std::shared_ptr /*event*/) +{ + // A restart begins a new run, and the temporary blueprint belongs to the old one + // (REQ-UI-BLUEPRINT-TEMP). The saved blueprints are not run state and stay. + m_temporaryBlueprint = std::nullopt; } Blueprint BlueprintLibrary::createBlueprintFromSelection() const diff --git a/src/ui/BlueprintLibrary.h b/src/ui/BlueprintLibrary.h index 33b7aab..cd9c94f 100644 --- a/src/ui/BlueprintLibrary.h +++ b/src/ui/BlueprintLibrary.h @@ -10,8 +10,10 @@ #include "BuildingId.h" #include "EventHandler.h" #include "GameConfig.h" +#include "GameResetEvent.h" #include "SelectionChangedEvent.h" -#include "TemporaryBlueprintRequestedEvent.h" +#include "TemporaryBlueprintCaptureRequestedEvent.h" +#include "TemporaryBlueprintPlaceRequestedEvent.h" class Simulation; class QWidget; @@ -24,9 +26,12 @@ class QWidget; // (REQ-UI-BLUEPRINT-DIALOG), and the modal dialogs that present them must be driven from // MainWindow, the only widget that can pause the game (ModalPauseScope) and raise the dim // overlay. This class is the model those dialogs read and mutate. -class BlueprintLibrary : public CombinedEventHandler +class BlueprintLibrary + : public CombinedEventHandler { public: // dialogParent parents the load-failure message box (REQ-UI-BLUEPRINT-LOAD) and is @@ -67,7 +72,11 @@ public: 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; Blueprint createBlueprintFromSelection() const; void loadFromDisk(); @@ -83,4 +92,10 @@ private: // Index of the blueprint currently in placement mode, so deleting it can exit that // mode (REQ-UI-BLUEPRINT-DELETE). nullopt = no saved blueprint is being placed. std::optional m_activeIndex; + // The one unnamed blueprint captured with C and re-placed with V + // (REQ-UI-BLUEPRINT-TEMP). Deliberately kept out of m_blueprints: it is never named, + // never listed in the selection dialog, and never written to blueprints.toml. It + // outlives its placement mode, so V can re-enter it. nullopt = none captured since + // startup or the last restart. + std::optional m_temporaryBlueprint; }; diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index ff1796f..4179b99 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -58,7 +58,7 @@ #include "EscapeMenuRequestedEvent.h" #include "TracePrintRequestedEvent.h" #include "BuildHotkeyPressedEvent.h" -#include "TemporaryBlueprintRequestedEvent.h" +#include "GameResetEvent.h" #include "BossWaveUpdatedEvent.h" #include "BuilderModeExitedEvent.h" #include "BlueprintModeExitedEvent.h" @@ -1502,6 +1502,10 @@ void GameWorldView::resetForNewGame() m_lastArtifactCount = -1; EventManager::getInstance()->sendEventImmediately( std::make_shared(std::vector{})); + // The one place a restart actually lands (the menu, game over and win dialogs only + // enqueue the command), so it is where presentation state belonging to the finished + // run is dropped -- e.g. the temporary blueprint (REQ-UI-BLUEPRINT-TEMP). + EventManager::getInstance()->sendEventImmediately(std::make_shared()); setGameSpeed(1.0); // Rebase the wall-clock time source so a fresh run starts from a clean time // base. Without this, wall time accumulated while a modal (Game Over, Win, or diff --git a/src/ui/InputMapper.cpp b/src/ui/InputMapper.cpp index c830e68..2f2c771 100644 --- a/src/ui/InputMapper.cpp +++ b/src/ui/InputMapper.cpp @@ -17,7 +17,8 @@ #include "PanDirectionChangedEvent.h" #include "PauseToggleRequestedEvent.h" #include "SpeedStepRequestedEvent.h" -#include "TemporaryBlueprintRequestedEvent.h" +#include "TemporaryBlueprintCaptureRequestedEvent.h" +#include "TemporaryBlueprintPlaceRequestedEvent.h" #include "TracePrintRequestedEvent.h" namespace @@ -100,9 +101,10 @@ bool InputMapper::handleKeyPress(QKeyEvent* event) } // Blueprint chords (REQ-UI-HOTKEYS). Checked ahead of the plain-key switch below, - // which binds bare A/D/W/S/R/Q/T and must not fire on a Ctrl chord. Both requests - // are decided by MainWindow, the only widget that can pause the game and dim the - // window for a modal. + // which binds bare A/D/W/S/R/Q/C/V and must not fire on a Ctrl chord -- bare C and V + // are the temporary-blueprint counterparts of these two (REQ-UI-BLUEPRINT-TEMP). Both + // requests are decided by MainWindow, the only widget that can pause the game and dim + // the window for a modal. if ((event->modifiers() & Qt::ControlModifier) != 0) { switch (event->key()) @@ -152,12 +154,18 @@ bool InputMapper::handleKeyPress(QKeyEvent* event) EventManager::getInstance()->sendEventImmediately( std::make_shared()); return true; - case Qt::Key_T: - // Request a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP). + case Qt::Key_C: + // Capture a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP). // The BlueprintLibrary owns the selection and blueprint-capture logic; it decides // whether anything placeable is selected and drives placement mode from there. EventManager::getInstance()->sendEventImmediately( - std::make_shared()); + std::make_shared()); + return true; + case Qt::Key_V: + // Re-enter placement mode for the temporary blueprint captured with C, if there is + // one (REQ-UI-BLUEPRINT-TEMP). The library holds it; nothing is captured here. + EventManager::getInstance()->sendEventImmediately( + std::make_shared()); return true; case Qt::Key_F3: EventManager::getInstance()->sendEventImmediately(