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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
2026-08-06 15:11:49 +02:00
parent f9c654b148
commit 51c6a4a567
9 changed files with 106 additions and 28 deletions

View File

@@ -10,6 +10,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/GameResetEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ArtifactCountChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/ArtifactCountChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/UnlockedBuildingsChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/UnlockedBuildingsChangedEvent.h
@@ -31,7 +32,8 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructModeToggleRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/DeconstructModeToggleRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ExitBlueprintModeRequestedEvent.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}/SpeedChangeRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/LayoutDialogRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/LayoutDialogRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionRequestedEvent.h

View File

@@ -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
{
};

View File

@@ -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
{
};

View File

@@ -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
{
};

View File

@@ -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
{
};

View File

@@ -116,22 +116,48 @@ void BlueprintLibrary::handleEvent(std::shared_ptr<const SelectionChangedEvent>
void BlueprintLibrary::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/) void BlueprintLibrary::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*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; m_activeIndex = std::nullopt;
} }
void BlueprintLibrary::handleEvent( void BlueprintLibrary::handleEvent(
std::shared_ptr<const TemporaryBlueprintRequestedEvent> /*event*/) std::shared_ptr<const TemporaryBlueprintCaptureRequestedEvent> /*event*/)
{ {
// Temporary blueprint (REQ-UI-BLUEPRINT-TEMP): build from the current selection and // C (REQ-UI-BLUEPRINT-TEMP): capture the current selection and enter placement mode
// enter placement mode without adding it to the list or persisting it. If nothing // for it, without naming it, listing it, or persisting it. If nothing player-placeable
// player-placeable is selected, do nothing. // is selected, do nothing at all -- in particular, keep the previous temporary
// blueprint, which V can still place.
Blueprint blueprint = createBlueprintFromSelection(); Blueprint blueprint = createBlueprintFromSelection();
if (blueprint.buildings.empty()) { return; } if (blueprint.buildings.empty()) { return; }
m_temporaryBlueprint = std::move(blueprint);
// No saved blueprint is active while a temporary one is being placed. // No saved blueprint is active while a temporary one is being placed.
m_activeIndex = std::nullopt; m_activeIndex = std::nullopt;
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<BlueprintPlacementRequestedEvent>(std::move(blueprint))); std::make_shared<BlueprintPlacementRequestedEvent>(*m_temporaryBlueprint));
}
void BlueprintLibrary::handleEvent(
std::shared_ptr<const TemporaryBlueprintPlaceRequestedEvent> /*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<BlueprintPlacementRequestedEvent>(*m_temporaryBlueprint));
}
void BlueprintLibrary::handleEvent(std::shared_ptr<const GameResetEvent> /*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 Blueprint BlueprintLibrary::createBlueprintFromSelection() const

View File

@@ -10,8 +10,10 @@
#include "BuildingId.h" #include "BuildingId.h"
#include "EventHandler.h" #include "EventHandler.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "GameResetEvent.h"
#include "SelectionChangedEvent.h" #include "SelectionChangedEvent.h"
#include "TemporaryBlueprintRequestedEvent.h" #include "TemporaryBlueprintCaptureRequestedEvent.h"
#include "TemporaryBlueprintPlaceRequestedEvent.h"
class Simulation; class Simulation;
class QWidget; class QWidget;
@@ -24,9 +26,12 @@ class QWidget;
// (REQ-UI-BLUEPRINT-DIALOG), and the modal dialogs that present them must be driven from // (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 // 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. // overlay. This class is the model those dialogs read and mutate.
class BlueprintLibrary : public CombinedEventHandler<SelectionChangedEvent, class BlueprintLibrary
: public CombinedEventHandler<SelectionChangedEvent,
BlueprintModeExitedEvent, BlueprintModeExitedEvent,
TemporaryBlueprintRequestedEvent> TemporaryBlueprintCaptureRequestedEvent,
TemporaryBlueprintPlaceRequestedEvent,
GameResetEvent>
{ {
public: public:
// dialogParent parents the load-failure message box (REQ-UI-BLUEPRINT-LOAD) and is // dialogParent parents the load-failure message box (REQ-UI-BLUEPRINT-LOAD) and is
@@ -67,7 +72,11 @@ public:
private: private:
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override; void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> event) override; void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> event) override;
void handleEvent(std::shared_ptr<const TemporaryBlueprintRequestedEvent> event) override; void handleEvent(
std::shared_ptr<const TemporaryBlueprintCaptureRequestedEvent> event) override;
void handleEvent(
std::shared_ptr<const TemporaryBlueprintPlaceRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const GameResetEvent> event) override;
Blueprint createBlueprintFromSelection() const; Blueprint createBlueprintFromSelection() const;
void loadFromDisk(); void loadFromDisk();
@@ -83,4 +92,10 @@ private:
// Index of the blueprint currently in placement mode, so deleting it can exit that // 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. // mode (REQ-UI-BLUEPRINT-DELETE). nullopt = no saved blueprint is being placed.
std::optional<int> m_activeIndex; std::optional<int> 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<Blueprint> m_temporaryBlueprint;
}; };

View File

@@ -58,7 +58,7 @@
#include "EscapeMenuRequestedEvent.h" #include "EscapeMenuRequestedEvent.h"
#include "TracePrintRequestedEvent.h" #include "TracePrintRequestedEvent.h"
#include "BuildHotkeyPressedEvent.h" #include "BuildHotkeyPressedEvent.h"
#include "TemporaryBlueprintRequestedEvent.h" #include "GameResetEvent.h"
#include "BossWaveUpdatedEvent.h" #include "BossWaveUpdatedEvent.h"
#include "BuilderModeExitedEvent.h" #include "BuilderModeExitedEvent.h"
#include "BlueprintModeExitedEvent.h" #include "BlueprintModeExitedEvent.h"
@@ -1502,6 +1502,10 @@ void GameWorldView::resetForNewGame()
m_lastArtifactCount = -1; m_lastArtifactCount = -1;
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<SelectionChangedEvent>(std::vector<BuildingId>{})); std::make_shared<SelectionChangedEvent>(std::vector<BuildingId>{}));
// 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<GameResetEvent>());
setGameSpeed(1.0); setGameSpeed(1.0);
// Rebase the wall-clock time source so a fresh run starts from a clean time // 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 // base. Without this, wall time accumulated while a modal (Game Over, Win, or

View File

@@ -17,7 +17,8 @@
#include "PanDirectionChangedEvent.h" #include "PanDirectionChangedEvent.h"
#include "PauseToggleRequestedEvent.h" #include "PauseToggleRequestedEvent.h"
#include "SpeedStepRequestedEvent.h" #include "SpeedStepRequestedEvent.h"
#include "TemporaryBlueprintRequestedEvent.h" #include "TemporaryBlueprintCaptureRequestedEvent.h"
#include "TemporaryBlueprintPlaceRequestedEvent.h"
#include "TracePrintRequestedEvent.h" #include "TracePrintRequestedEvent.h"
namespace namespace
@@ -100,9 +101,10 @@ bool InputMapper::handleKeyPress(QKeyEvent* event)
} }
// Blueprint chords (REQ-UI-HOTKEYS). Checked ahead of the plain-key switch below, // 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 // which binds bare A/D/W/S/R/Q/C/V and must not fire on a Ctrl chord -- bare C and V
// are decided by MainWindow, the only widget that can pause the game and dim the // are the temporary-blueprint counterparts of these two (REQ-UI-BLUEPRINT-TEMP). Both
// window for a modal. // 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) if ((event->modifiers() & Qt::ControlModifier) != 0)
{ {
switch (event->key()) switch (event->key())
@@ -152,12 +154,18 @@ bool InputMapper::handleKeyPress(QKeyEvent* event)
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<ModeCancelRequestedEvent>()); std::make_shared<ModeCancelRequestedEvent>());
return true; return true;
case Qt::Key_T: case Qt::Key_C:
// Request a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP). // Capture a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP).
// The BlueprintLibrary owns the selection and blueprint-capture logic; it decides // The BlueprintLibrary owns the selection and blueprint-capture logic; it decides
// whether anything placeable is selected and drives placement mode from there. // whether anything placeable is selected and drives placement mode from there.
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<TemporaryBlueprintRequestedEvent>()); std::make_shared<TemporaryBlueprintCaptureRequestedEvent>());
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<TemporaryBlueprintPlaceRequestedEvent>());
return true; return true;
case Qt::Key_F3: case Qt::Key_F3:
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(