extract BuildModeController, fixing a silent blueprint exit
This commit is contained in:
261
src/lib/core/BuildModeController.cpp
Normal file
261
src/lib/core/BuildModeController.cpp
Normal 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);
|
||||
}
|
||||
123
src/lib/core/BuildModeController.h
Normal file
123
src/lib/core/BuildModeController.h
Normal 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;
|
||||
};
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user