rekey the temporary blueprint to C, add V to re-place it

This commit is contained in:
2026-08-06 19:07:13 +02:00
parent 18cfe238f6
commit fd6a7c5815
10 changed files with 114 additions and 31 deletions

View File

@@ -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

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*/)
{
// 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<const TemporaryBlueprintRequestedEvent> /*event*/)
std::shared_ptr<const TemporaryBlueprintCaptureRequestedEvent> /*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<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

View File

@@ -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<SelectionChangedEvent,
BlueprintModeExitedEvent,
TemporaryBlueprintRequestedEvent>
class BlueprintLibrary
: public CombinedEventHandler<SelectionChangedEvent,
BlueprintModeExitedEvent,
TemporaryBlueprintCaptureRequestedEvent,
TemporaryBlueprintPlaceRequestedEvent,
GameResetEvent>
{
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<const SelectionChangedEvent> 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;
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<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 "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<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);
// 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

View File

@@ -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<ModeCancelRequestedEvent>());
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<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;
case Qt::Key_F3:
EventManager::getInstance()->sendEventImmediately(