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
134 lines
5.7 KiB
C++
134 lines
5.7 KiB
C++
#pragma once
|
|
|
|
#include <optional>
|
|
#include <vector>
|
|
|
|
#include <QPoint>
|
|
|
|
#include "BeltDragPath.h"
|
|
#include "Blueprint.h"
|
|
#include "BuildingId.h"
|
|
#include "BuildingType.h"
|
|
#include "Rotation.h"
|
|
|
|
// Which of the mutually exclusive world-interaction modes is active
|
|
// (REQ-UI-HOTKEYS, REQ-BLD-GHOST, REQ-UI-BLUEPRINT-PLACE, REQ-BLD-DECONSTRUCT).
|
|
enum class BuildMode
|
|
{
|
|
None, // plain selection
|
|
Builder, // placing one building type, ghost following the cursor
|
|
Blueprint, // placing a saved multi-building layout
|
|
Deconstruct // marking buildings for demolition
|
|
};
|
|
|
|
// The active build mode and the transient state that belongs to it.
|
|
//
|
|
// These modes were previously three independent flags, and every entry point
|
|
// cleared the other two by hand — inconsistently, which is how entering builder
|
|
// mode came to drop a blueprint without announcing it. Here exclusivity is
|
|
// structural: one mode is active, and every transition runs through enterMode(),
|
|
// which exits whatever was active first and publishes the same events regardless
|
|
// of which way the player got there.
|
|
//
|
|
// Everything needing the simulation — placement validity, tunnel matching, belt
|
|
// path building — stays with the caller, which computes and hands back the result
|
|
// (setGhostValidity, setTunnelGhost, setBeltDragPath). That keeps this a plain
|
|
// value that can be tested without a world.
|
|
class BuildModeController
|
|
{
|
|
public:
|
|
BuildMode getMode() const;
|
|
bool isBuilderMode() const;
|
|
bool isBlueprintMode() const;
|
|
bool isDeconstructMode() const;
|
|
|
|
// --- transitions ----------------------------------------------------------
|
|
// Each leaves the previously active mode with its proper exit event.
|
|
void enterBuilderMode(BuildingType type);
|
|
void enterBlueprintMode(Blueprint blueprint);
|
|
// Leaves deconstruct mode if it is active, enters it otherwise
|
|
// (REQ-BLD-DECONSTRUCT-CLICK).
|
|
void toggleDeconstructMode();
|
|
void exitBuilderMode();
|
|
void exitBlueprintMode();
|
|
// Backs out of whichever mode is active, if any (the Q key and right-click).
|
|
void exitCurrentMode();
|
|
|
|
// --- builder mode ---------------------------------------------------------
|
|
// Only meaningful while isBuilderMode().
|
|
BuildingType getBuilderType() const;
|
|
// True while the builder type is TunnelEntry, where the ghost resolves to an
|
|
// entry or an exit by hovered position (REQ-BLD-TUNNEL-MODE).
|
|
bool isTunnelMode() const;
|
|
// The type the ghost currently represents: the position-resolved tunnel type in
|
|
// tunnel mode, the plain builder type otherwise.
|
|
BuildingType getEffectiveBuilderType() const;
|
|
|
|
QPoint getGhostTile() const;
|
|
Rotation getGhostRotation() const;
|
|
bool isGhostValid() const;
|
|
void setGhostTile(QPoint tile);
|
|
void setGhostValidity(bool valid);
|
|
// Turns the ghost one quarter turn. Validity is not rechecked here; the caller
|
|
// does that and calls setGhostValidity, because only it can see the world.
|
|
void rotateGhost(bool clockwise);
|
|
|
|
BuildingType getTunnelGhostType() const;
|
|
const std::optional<QPoint>& getTunnelPartnerTile() const;
|
|
void setTunnelGhost(BuildingType resolvedType, std::optional<QPoint> partnerTile);
|
|
|
|
// --- belt drag placement (REQ-BLD-BELT-DRAG) ------------------------------
|
|
bool isDraggingBelt() const;
|
|
QPoint getBeltDragAnchor() const;
|
|
const std::vector<BeltPathTile>& getBeltDragPath() const;
|
|
void beginBeltDrag(QPoint anchorTile);
|
|
void setBeltDragPath(std::vector<BeltPathTile> path);
|
|
// Drops the drag without placing anything, staying in builder mode.
|
|
void cancelBeltDrag();
|
|
|
|
// --- blueprint mode -------------------------------------------------------
|
|
// Only meaningful while isBlueprintMode().
|
|
const Blueprint& getBlueprint() const;
|
|
// Mutable so the caller can rotate the layout in place; rotating a blueprint
|
|
// needs building footprints from the config, which does not belong here.
|
|
Blueprint& getMutableBlueprint();
|
|
QPoint getBlueprintGhostTile() const;
|
|
void setBlueprintGhostTile(QPoint tile);
|
|
|
|
// Whether the ghost under the cursor would hand its settings to the building
|
|
// already there rather than place anything (REQ-UI-BLUEPRINT-TRANSFER). Classifying
|
|
// it needs the factory state, so the caller resolves it and stores the answer here,
|
|
// as with setGhostValidity. Kept here rather than recomputed per reader so the
|
|
// click, the ghost's colour, and the controls panel cannot disagree about what the
|
|
// cursor is over.
|
|
bool isHoveredGhostTransfer() const;
|
|
void setHoveredGhostTransfer(bool transfer);
|
|
|
|
// --- deconstruct mode -----------------------------------------------------
|
|
const std::optional<BuildingId>& getDeconstructHoverBuildingId() const;
|
|
void setDeconstructHoverBuildingId(std::optional<BuildingId> id);
|
|
|
|
private:
|
|
// The single transition point: leaves the active mode, then enters `mode`.
|
|
void enterMode(BuildMode mode);
|
|
|
|
BuildMode m_mode = BuildMode::None;
|
|
|
|
BuildingType m_builderType = BuildingType::Belt;
|
|
QPoint m_ghostTile;
|
|
Rotation m_ghostRotation = Rotation::East;
|
|
bool m_ghostValid = false;
|
|
BuildingType m_tunnelGhostType = BuildingType::TunnelEntry;
|
|
std::optional<QPoint> m_tunnelPartnerTile;
|
|
|
|
bool m_draggingBelt = false;
|
|
QPoint m_beltDragAnchor;
|
|
std::vector<BeltPathTile> m_beltDragPath;
|
|
|
|
Blueprint m_blueprint;
|
|
QPoint m_blueprintGhostTile;
|
|
bool m_hoveredGhostIsTransfer = false;
|
|
|
|
std::optional<BuildingId> m_deconstructHoverBuildingId;
|
|
};
|