extract BuildModeController, fixing a silent blueprint exit

This commit is contained in:
2026-08-05 20:41:57 +02:00
parent 202f583067
commit 26d7448492
7 changed files with 803 additions and 204 deletions

View File

@@ -0,0 +1,261 @@
#include "BuildModeController.h"
#include <memory>
#include <utility>
#include "BlueprintModeExitedEvent.h"
#include "BuilderModeExitedEvent.h"
#include "DeconstructModeChangedEvent.h"
#include "EventManager.h"
namespace
{
Rotation rotateClockwise(Rotation rotation)
{
switch (rotation)
{
case Rotation::North: return Rotation::East;
case Rotation::East: return Rotation::South;
case Rotation::South: return Rotation::West;
case Rotation::West: return Rotation::North;
}
return Rotation::East;
}
Rotation rotateCounterClockwise(Rotation rotation)
{
switch (rotation)
{
case Rotation::North: return Rotation::West;
case Rotation::East: return Rotation::North;
case Rotation::South: return Rotation::East;
case Rotation::West: return Rotation::South;
}
return Rotation::East;
}
} // namespace
BuildMode BuildModeController::getMode() const
{
return m_mode;
}
bool BuildModeController::isBuilderMode() const
{
return m_mode == BuildMode::Builder;
}
bool BuildModeController::isBlueprintMode() const
{
return m_mode == BuildMode::Blueprint;
}
bool BuildModeController::isDeconstructMode() const
{
return m_mode == BuildMode::Deconstruct;
}
void BuildModeController::enterMode(BuildMode mode)
{
if (m_mode == mode) { return; }
// Leave the current mode properly, so its widget hears about it however the
// player left. Each exit clears only its own state.
switch (m_mode)
{
case BuildMode::Builder:
m_draggingBelt = false;
m_beltDragPath.clear();
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BuilderModeExitedEvent>());
break;
case BuildMode::Blueprint:
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BlueprintModeExitedEvent>());
break;
case BuildMode::Deconstruct:
m_deconstructHoverBuildingId.reset();
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DeconstructModeChangedEvent>(false));
break;
case BuildMode::None:
break;
}
m_mode = mode;
if (mode == BuildMode::Deconstruct)
{
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DeconstructModeChangedEvent>(true));
}
}
void BuildModeController::enterBuilderMode(BuildingType type)
{
enterMode(BuildMode::Builder);
m_builderType = type;
m_ghostRotation = Rotation::East;
m_ghostValid = false;
m_tunnelGhostType = BuildingType::TunnelEntry;
m_tunnelPartnerTile.reset();
}
void BuildModeController::enterBlueprintMode(Blueprint blueprint)
{
enterMode(BuildMode::Blueprint);
// The layout starts where the builder ghost last was, so switching from a
// building to a blueprint does not jump the preview across the world.
m_blueprintGhostTile = m_ghostTile;
m_blueprint = std::move(blueprint);
}
void BuildModeController::toggleDeconstructMode()
{
enterMode(isDeconstructMode() ? BuildMode::None : BuildMode::Deconstruct);
}
void BuildModeController::exitBuilderMode()
{
if (!isBuilderMode()) { return; }
enterMode(BuildMode::None);
}
void BuildModeController::exitBlueprintMode()
{
if (!isBlueprintMode()) { return; }
enterMode(BuildMode::None);
}
void BuildModeController::exitCurrentMode()
{
enterMode(BuildMode::None);
}
BuildingType BuildModeController::getBuilderType() const
{
return m_builderType;
}
bool BuildModeController::isTunnelMode() const
{
return isBuilderMode() && m_builderType == BuildingType::TunnelEntry;
}
BuildingType BuildModeController::getEffectiveBuilderType() const
{
return isTunnelMode() ? m_tunnelGhostType : m_builderType;
}
QPoint BuildModeController::getGhostTile() const
{
return m_ghostTile;
}
Rotation BuildModeController::getGhostRotation() const
{
return m_ghostRotation;
}
bool BuildModeController::isGhostValid() const
{
return m_ghostValid;
}
void BuildModeController::setGhostTile(QPoint tile)
{
m_ghostTile = tile;
}
void BuildModeController::setGhostValidity(bool valid)
{
m_ghostValid = valid;
}
void BuildModeController::rotateGhost(bool clockwise)
{
m_ghostRotation = clockwise ? rotateClockwise(m_ghostRotation)
: rotateCounterClockwise(m_ghostRotation);
}
BuildingType BuildModeController::getTunnelGhostType() const
{
return m_tunnelGhostType;
}
const std::optional<QPoint>& BuildModeController::getTunnelPartnerTile() const
{
return m_tunnelPartnerTile;
}
void BuildModeController::setTunnelGhost(BuildingType resolvedType,
std::optional<QPoint> partnerTile)
{
m_tunnelGhostType = resolvedType;
m_tunnelPartnerTile = std::move(partnerTile);
}
bool BuildModeController::isDraggingBelt() const
{
return m_draggingBelt;
}
QPoint BuildModeController::getBeltDragAnchor() const
{
return m_beltDragAnchor;
}
const std::vector<BeltPathTile>& BuildModeController::getBeltDragPath() const
{
return m_beltDragPath;
}
void BuildModeController::beginBeltDrag(QPoint anchorTile)
{
m_draggingBelt = true;
m_beltDragAnchor = anchorTile;
}
void BuildModeController::setBeltDragPath(std::vector<BeltPathTile> path)
{
m_beltDragPath = std::move(path);
}
void BuildModeController::cancelBeltDrag()
{
m_draggingBelt = false;
m_beltDragPath.clear();
}
const Blueprint& BuildModeController::getBlueprint() const
{
return m_blueprint;
}
Blueprint& BuildModeController::getMutableBlueprint()
{
return m_blueprint;
}
QPoint BuildModeController::getBlueprintGhostTile() const
{
return m_blueprintGhostTile;
}
void BuildModeController::setBlueprintGhostTile(QPoint tile)
{
m_blueprintGhostTile = tile;
}
const std::optional<BuildingId>&
BuildModeController::getDeconstructHoverBuildingId() const
{
return m_deconstructHoverBuildingId;
}
void BuildModeController::setDeconstructHoverBuildingId(std::optional<BuildingId> id)
{
m_deconstructHoverBuildingId = std::move(id);
}

View File

@@ -0,0 +1,123 @@
#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);
// --- 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;
std::optional<BuildingId> m_deconstructHoverBuildingId;
};

View File

@@ -17,6 +17,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
PARENT_SCOPE
)
@@ -31,6 +32,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
PARENT_SCOPE
)