The controls panel needs to say what each key does right now, and a panel that keeps its own list of that is a list that goes stale. So the list moves into lib/core/ControlAction.h: which actions exist, what each is bound to, and when each does something. InputMapper stops deciding that and switches on the resolved action instead, so the panel and the key handling cannot disagree about what Q means -- there is only one place that says. The table declares; it never performs. It holds no simulation access, fires no events, and names nothing: display strings live in the ui target, which formats the bindings this hands it, so a badge is rendered from the real binding rather than typed beside it. What an action *does* stays exactly where it was. ControlContext is the snapshot the rules read, which is what keeps this testable without a world. Two of its facts come from BlueprintLibrary, which is built after the world view and so arrives by setter; one comes from the new hovered-transfer flag on BuildModeController, resolved once on mouse-move through the same classifier the click and the ghost colour already use. Behaviour is unchanged, deliberately. Ctrl still separates the chords and every other modifier is still ignored, so Shift+A pans as before; matching modifiers exactly would have silently swallowed those presses. Build hotkeys and F3/F4 stay outside the table -- the first are advertised on the build buttons and already derive their badges from the handler's own table, the second are development controls the panel must never offer. The tests are the point of putting this in lib: every row's bindings must resolve back to that row's action in that same context, which fails the moment a shown row and its handler part ways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
107 lines
4.6 KiB
C++
107 lines
4.6 KiB
C++
#pragma once
|
|
|
|
#include <optional>
|
|
#include <vector>
|
|
|
|
#include <QString>
|
|
|
|
#include "Blueprint.h"
|
|
#include "BlueprintModeExitedEvent.h"
|
|
#include "BuildingId.h"
|
|
#include "EventHandler.h"
|
|
#include "GameConfig.h"
|
|
#include "GameResetEvent.h"
|
|
#include "SelectionChangedEvent.h"
|
|
#include "TemporaryBlueprintCaptureRequestedEvent.h"
|
|
#include "TemporaryBlueprintPlaceRequestedEvent.h"
|
|
|
|
class Simulation;
|
|
class QWidget;
|
|
|
|
// The player's saved blueprints: the list itself, its persistence (REQ-UI-BLUEPRINT-SAVE,
|
|
// REQ-UI-BLUEPRINT-LOAD), capture from the current selection (REQ-UI-BLUEPRINT-CREATE,
|
|
// REQ-UI-BLUEPRINT-TEMP), and entry into blueprint placement mode.
|
|
//
|
|
// Deliberately not a widget: blueprints have no permanent place on screen any more
|
|
// (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,
|
|
TemporaryBlueprintCaptureRequestedEvent,
|
|
TemporaryBlueprintPlaceRequestedEvent,
|
|
GameResetEvent>
|
|
{
|
|
public:
|
|
// dialogParent parents the load-failure message box (REQ-UI-BLUEPRINT-LOAD) and is
|
|
// not owned.
|
|
BlueprintLibrary(Simulation* sim, const GameConfig* config, QWidget* dialogParent);
|
|
~BlueprintLibrary();
|
|
|
|
// True when the current selection holds at least one player-placeable building or
|
|
// construction site -- the condition under which Ctrl+C has any effect
|
|
// (REQ-UI-BLUEPRINT-CREATE).
|
|
bool getCanCaptureSelection() const;
|
|
|
|
// True once C has captured something -- the condition under which V does anything
|
|
// (REQ-UI-BLUEPRINT-TEMP), and so the condition under which the controls panel
|
|
// offers it (REQ-UI-CONTROLS-ACCURACY).
|
|
bool getHasTemporaryBlueprint() const;
|
|
|
|
// Captures the current selection under the given name and appends it to the list.
|
|
// Silently does nothing when nothing player-placeable is selected.
|
|
void saveSelectionAs(const QString& name);
|
|
|
|
const std::vector<Blueprint>& getBlueprints() const;
|
|
|
|
// The blueprint's contents line, ready for its card: one "<name> x <count>" entry
|
|
// per building type, comma-separated, ordered by summarizeBlueprintContents
|
|
// (REQ-UI-BLUEPRINT-CARD).
|
|
QString getContentsSummary(int index) const;
|
|
|
|
// Sum of the constituent buildings' placement costs (REQ-UI-BLUEPRINT-CARD).
|
|
int getCost(int index) const;
|
|
|
|
// Whether the player can currently afford the blueprint's total cost, which is what
|
|
// enables or greys out its card (REQ-UI-BLUEPRINT-CARD).
|
|
bool getCanAfford(int index) const;
|
|
|
|
// Removes the blueprint, exiting blueprint placement mode if it was the active one
|
|
// (REQ-UI-BLUEPRINT-DELETE).
|
|
void remove(int index);
|
|
|
|
// Enters blueprint placement mode for the blueprint (REQ-UI-BLUEPRINT-MODE).
|
|
void beginPlacement(int index);
|
|
|
|
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 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();
|
|
void saveToDisk() const;
|
|
|
|
// The simulation is the single source of truth for the block stock and the
|
|
// selection; the change events are only refresh signals.
|
|
Simulation* m_sim;
|
|
const GameConfig* m_config;
|
|
QWidget* m_dialogParent;
|
|
std::vector<BuildingId> m_selectedBuildingIds;
|
|
std::vector<Blueprint> m_blueprints;
|
|
// 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;
|
|
};
|