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(