extract BuildModeController, fixing a silent blueprint exit
Builder, blueprint and deconstruct were three independent flags, and every entry point cleared the other two by hand. The copies had drifted, and one had a real bug: enterBuilderMode reset the blueprint directly instead of calling exitBlueprintMode, so BlueprintModeExitedEvent never fired and BlueprintPanel never ran clearActiveBlueprintButton. Activating a blueprint and then picking a building left the blueprint button highlighted for a mode that had ended. Exclusivity is now structural. One mode is active, and every transition runs through enterMode(), which exits whatever was active first. Which mode the player switches to can no longer change what the mode they left announces - a test covers all four crossings, plus the blueprint regression above. The controller also owns the state that belongs to a mode and had to be cleared with it: ghost tile/rotation/validity, the resolved tunnel end and its completion partner, the belt drag, and the deconstruct hover. Those were the things the hand-written resets kept forgetting. Everything needing the simulation stays in GameWorldView - placement validity, tunnel matching, belt path building - and is handed back through setGhostValidity, setTunnelGhost and setBeltDragPath. That is what keeps the controller a plain value with tests. Two smaller behaviour changes, both dropping redundant events. Restart now announces only the mode that was actually active rather than all three exits unconditionally; and entering builder or blueprint mode no longer publishes DeconstructModeChangedEvent(false) when deconstruct mode was not on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
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}/WorldCoordinates.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
301
src/test/BuildModeControllerTest.cpp
Normal file
301
src/test/BuildModeControllerTest.cpp
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "BlueprintModeExitedEvent.h"
|
||||||
|
#include "BuildModeController.h"
|
||||||
|
#include "BuilderModeExitedEvent.h"
|
||||||
|
#include "DeconstructModeChangedEvent.h"
|
||||||
|
#include "EventHandler.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Records the mode events, which are the half of this class's contract that the
|
||||||
|
// panels depend on: a mode that ends without announcing it leaves its button
|
||||||
|
// stuck highlighted.
|
||||||
|
class ModeEventSpy : public CombinedEventHandler<BuilderModeExitedEvent,
|
||||||
|
BlueprintModeExitedEvent,
|
||||||
|
DeconstructModeChangedEvent>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ModeEventSpy() { registerForEvents(); }
|
||||||
|
~ModeEventSpy() { unregisterForEvents(); }
|
||||||
|
|
||||||
|
int builderExits = 0;
|
||||||
|
int blueprintExits = 0;
|
||||||
|
int deconstructChanges = 0;
|
||||||
|
bool lastDeconstructActive = false;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> /*event*/) override
|
||||||
|
{
|
||||||
|
++builderExits;
|
||||||
|
}
|
||||||
|
void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/) override
|
||||||
|
{
|
||||||
|
++blueprintExits;
|
||||||
|
}
|
||||||
|
void handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event) override
|
||||||
|
{
|
||||||
|
++deconstructChanges;
|
||||||
|
lastDeconstructActive = event->active;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static Blueprint makeBlueprint()
|
||||||
|
{
|
||||||
|
Blueprint blueprint;
|
||||||
|
BlueprintBuilding building;
|
||||||
|
building.type = BuildingType::Belt;
|
||||||
|
building.offset = QPoint(0, 0);
|
||||||
|
building.rotation = Rotation::East;
|
||||||
|
blueprint.buildings.push_back(building);
|
||||||
|
return blueprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Exclusivity
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Only one mode is active at a time", "[buildmode]")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
REQUIRE(controller.getMode() == BuildMode::None);
|
||||||
|
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
REQUIRE(controller.isBuilderMode());
|
||||||
|
REQUIRE_FALSE(controller.isBlueprintMode());
|
||||||
|
REQUIRE_FALSE(controller.isDeconstructMode());
|
||||||
|
|
||||||
|
controller.enterBlueprintMode(makeBlueprint());
|
||||||
|
REQUIRE(controller.isBlueprintMode());
|
||||||
|
REQUIRE_FALSE(controller.isBuilderMode());
|
||||||
|
|
||||||
|
controller.toggleDeconstructMode();
|
||||||
|
REQUIRE(controller.isDeconstructMode());
|
||||||
|
REQUIRE_FALSE(controller.isBlueprintMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Entering builder mode announces that blueprint mode ended", "[buildmode]")
|
||||||
|
{
|
||||||
|
// Regression: entering builder mode used to drop the blueprint silently, so the
|
||||||
|
// blueprint panel kept its button highlighted for a mode that was over.
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBlueprintMode(makeBlueprint());
|
||||||
|
|
||||||
|
ModeEventSpy spy;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
|
||||||
|
REQUIRE(spy.blueprintExits == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Every mode announces its exit however it is left", "[buildmode]")
|
||||||
|
{
|
||||||
|
// The point of routing all transitions through one place: which mode the player
|
||||||
|
// switches to must not change what the mode they left announces.
|
||||||
|
SECTION("builder, left for a blueprint")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
ModeEventSpy spy;
|
||||||
|
controller.enterBlueprintMode(makeBlueprint());
|
||||||
|
REQUIRE(spy.builderExits == 1);
|
||||||
|
}
|
||||||
|
SECTION("builder, left for deconstruct")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
ModeEventSpy spy;
|
||||||
|
controller.toggleDeconstructMode();
|
||||||
|
REQUIRE(spy.builderExits == 1);
|
||||||
|
}
|
||||||
|
SECTION("blueprint, left for deconstruct")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBlueprintMode(makeBlueprint());
|
||||||
|
ModeEventSpy spy;
|
||||||
|
controller.toggleDeconstructMode();
|
||||||
|
REQUIRE(spy.blueprintExits == 1);
|
||||||
|
}
|
||||||
|
SECTION("deconstruct, left for builder")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.toggleDeconstructMode();
|
||||||
|
ModeEventSpy spy;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
REQUIRE(spy.deconstructChanges == 1);
|
||||||
|
REQUIRE_FALSE(spy.lastDeconstructActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Switching between builder types stays in builder mode", "[buildmode]")
|
||||||
|
{
|
||||||
|
// Picking a different building is not leaving the mode, so nothing is announced.
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
|
||||||
|
ModeEventSpy spy;
|
||||||
|
controller.enterBuilderMode(BuildingType::Splitter);
|
||||||
|
|
||||||
|
REQUIRE(controller.getBuilderType() == BuildingType::Splitter);
|
||||||
|
REQUIRE(spy.builderExits == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Exiting a mode that is not active does nothing", "[buildmode]")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
|
||||||
|
ModeEventSpy spy;
|
||||||
|
controller.exitBlueprintMode();
|
||||||
|
|
||||||
|
REQUIRE(controller.isBuilderMode());
|
||||||
|
REQUIRE(spy.blueprintExits == 0);
|
||||||
|
REQUIRE(spy.builderExits == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Deconstruct mode toggles off and announces both edges", "[buildmode]")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
|
||||||
|
ModeEventSpy spy;
|
||||||
|
controller.toggleDeconstructMode();
|
||||||
|
REQUIRE(controller.isDeconstructMode());
|
||||||
|
REQUIRE(spy.lastDeconstructActive);
|
||||||
|
|
||||||
|
controller.toggleDeconstructMode();
|
||||||
|
REQUIRE(controller.getMode() == BuildMode::None);
|
||||||
|
REQUIRE_FALSE(spy.lastDeconstructActive);
|
||||||
|
REQUIRE(spy.deconstructChanges == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Leaving the current mode works from any of them", "[buildmode]")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
controller.exitCurrentMode();
|
||||||
|
REQUIRE(controller.getMode() == BuildMode::None);
|
||||||
|
|
||||||
|
controller.enterBlueprintMode(makeBlueprint());
|
||||||
|
controller.exitCurrentMode();
|
||||||
|
REQUIRE(controller.getMode() == BuildMode::None);
|
||||||
|
|
||||||
|
controller.toggleDeconstructMode();
|
||||||
|
controller.exitCurrentMode();
|
||||||
|
REQUIRE(controller.getMode() == BuildMode::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// State cleared on transition
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Leaving builder mode drops an in-progress belt drag", "[buildmode]")
|
||||||
|
{
|
||||||
|
// A drag surviving the mode change would place belts on the next release.
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
controller.beginBeltDrag(QPoint(3, 4));
|
||||||
|
controller.setBeltDragPath({BeltPathTile{QPoint(3, 4), Rotation::East}});
|
||||||
|
REQUIRE(controller.isDraggingBelt());
|
||||||
|
|
||||||
|
controller.toggleDeconstructMode();
|
||||||
|
|
||||||
|
REQUIRE_FALSE(controller.isDraggingBelt());
|
||||||
|
REQUIRE(controller.getBeltDragPath().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Leaving deconstruct mode drops the hovered building", "[buildmode]")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.toggleDeconstructMode();
|
||||||
|
controller.setDeconstructHoverBuildingId(BuildingId(4));
|
||||||
|
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(controller.getDeconstructHoverBuildingId().has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Entering builder mode resets the ghost", "[buildmode]")
|
||||||
|
{
|
||||||
|
// A fresh builder starts facing East and invalid until the first hover, rather
|
||||||
|
// than inheriting the previous building's facing (REQ-BLD-GHOST).
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
controller.rotateGhost(true);
|
||||||
|
controller.setGhostValidity(true);
|
||||||
|
|
||||||
|
controller.enterBuilderMode(BuildingType::Splitter);
|
||||||
|
|
||||||
|
REQUIRE(controller.getGhostRotation() == Rotation::East);
|
||||||
|
REQUIRE_FALSE(controller.isGhostValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Cancelling a belt drag stays in builder mode", "[buildmode]")
|
||||||
|
{
|
||||||
|
// Right-click during a drag abandons the path but keeps the belt selected
|
||||||
|
// (REQ-BLD-BELT-DRAG).
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
controller.beginBeltDrag(QPoint(1, 1));
|
||||||
|
|
||||||
|
ModeEventSpy spy;
|
||||||
|
controller.cancelBeltDrag();
|
||||||
|
|
||||||
|
REQUIRE(controller.isBuilderMode());
|
||||||
|
REQUIRE_FALSE(controller.isDraggingBelt());
|
||||||
|
REQUIRE(spy.builderExits == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ghost and tunnel state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Rotating the ghost cycles through the four facings", "[buildmode]")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
|
||||||
|
controller.rotateGhost(true);
|
||||||
|
REQUIRE(controller.getGhostRotation() == Rotation::South);
|
||||||
|
controller.rotateGhost(true);
|
||||||
|
REQUIRE(controller.getGhostRotation() == Rotation::West);
|
||||||
|
controller.rotateGhost(false);
|
||||||
|
REQUIRE(controller.getGhostRotation() == Rotation::South);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Tunnel mode is the tunnel entry builder type", "[buildmode]")
|
||||||
|
{
|
||||||
|
// REQ-BLD-TUNNEL-MODE: one builder type covers both tunnel ends.
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
REQUIRE_FALSE(controller.isTunnelMode());
|
||||||
|
|
||||||
|
controller.enterBuilderMode(BuildingType::TunnelEntry);
|
||||||
|
REQUIRE(controller.isTunnelMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("The effective builder type follows the resolved tunnel end", "[buildmode]")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::TunnelEntry);
|
||||||
|
REQUIRE(controller.getEffectiveBuilderType() == BuildingType::TunnelEntry);
|
||||||
|
|
||||||
|
controller.setTunnelGhost(BuildingType::TunnelExit, QPoint(5, 5));
|
||||||
|
REQUIRE(controller.getEffectiveBuilderType() == BuildingType::TunnelExit);
|
||||||
|
REQUIRE(controller.getTunnelPartnerTile() == QPoint(5, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A non-tunnel builder ignores any resolved tunnel end", "[buildmode]")
|
||||||
|
{
|
||||||
|
BuildModeController controller;
|
||||||
|
controller.enterBuilderMode(BuildingType::TunnelEntry);
|
||||||
|
controller.setTunnelGhost(BuildingType::TunnelExit, QPoint(5, 5));
|
||||||
|
|
||||||
|
controller.enterBuilderMode(BuildingType::Belt);
|
||||||
|
|
||||||
|
REQUIRE(controller.getEffectiveBuilderType() == BuildingType::Belt);
|
||||||
|
REQUIRE_FALSE(controller.getTunnelPartnerTile().has_value());
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ add_files(
|
|||||||
WorldCoordinatesTest.cpp
|
WorldCoordinatesTest.cpp
|
||||||
WorldCameraTest.cpp
|
WorldCameraTest.cpp
|
||||||
SelectionControllerTest.cpp
|
SelectionControllerTest.cpp
|
||||||
|
BuildModeControllerTest.cpp
|
||||||
BuildingTest.cpp
|
BuildingTest.cpp
|
||||||
BuildingConfigTest.cpp
|
BuildingConfigTest.cpp
|
||||||
ShipTest.cpp
|
ShipTest.cpp
|
||||||
|
|||||||
@@ -199,10 +199,6 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
|
|||||||
, m_gameSpeedMultiplier(1.0)
|
, m_gameSpeedMultiplier(1.0)
|
||||||
, m_prevNonZeroSpeed(1.0)
|
, m_prevNonZeroSpeed(1.0)
|
||||||
, m_camera(config->world.scroll, config->world.regions)
|
, m_camera(config->world.scroll, config->world.regions)
|
||||||
, m_ghostRotation(Rotation::East)
|
|
||||||
, m_ghostValid(false)
|
|
||||||
, m_dragging(false)
|
|
||||||
, m_deconstructMode(false)
|
|
||||||
, m_debugDraw(false)
|
, m_debugDraw(false)
|
||||||
, m_rng(std::random_device{}())
|
, m_rng(std::random_device{}())
|
||||||
, m_boxSelecting(false)
|
, m_boxSelecting(false)
|
||||||
@@ -732,7 +728,7 @@ void GameWorldView::stepSpeed(int delta)
|
|||||||
|
|
||||||
void GameWorldView::placeBlueprintAtTile(QPoint center)
|
void GameWorldView::placeBlueprintAtTile(QPoint center)
|
||||||
{
|
{
|
||||||
const Blueprint& bp = *m_blueprintMode;
|
const Blueprint& bp = m_buildMode.getBlueprint();
|
||||||
|
|
||||||
for (const BlueprintBuilding& bb : bp.buildings)
|
for (const BlueprintBuilding& bb : bp.buildings)
|
||||||
{
|
{
|
||||||
@@ -819,16 +815,6 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GameWorldView::inTunnelMode() const
|
|
||||||
{
|
|
||||||
return m_builderType.has_value() && *m_builderType == BuildingType::TunnelEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildingType GameWorldView::effectiveBuilderType() const
|
|
||||||
{
|
|
||||||
return inTunnelMode() ? m_tunnelGhostType : *m_builderType;
|
|
||||||
}
|
|
||||||
|
|
||||||
TunnelTileMap GameWorldView::collectTunnelTiles() const
|
TunnelTileMap GameWorldView::collectTunnelTiles() const
|
||||||
{
|
{
|
||||||
// Index every tunnel entry/exit — built or still a construction site — by its
|
// Index every tunnel entry/exit — built or still a construction site — by its
|
||||||
@@ -864,13 +850,11 @@ TunnelLookup GameWorldView::makeTunnelLookup(const TunnelTileMap& tunnels)
|
|||||||
|
|
||||||
void GameWorldView::updateTunnelGhost()
|
void GameWorldView::updateTunnelGhost()
|
||||||
{
|
{
|
||||||
m_tunnelGhostType = BuildingType::TunnelEntry;
|
|
||||||
m_tunnelPartnerTile.reset();
|
|
||||||
|
|
||||||
// The connection preview and entry/exit switch only apply at a valid placement
|
// The connection preview and entry/exit switch only apply at a valid placement
|
||||||
// (REQ-BLD-TUNNEL-MODE); at an invalid position the ghost stays a plain entry.
|
// (REQ-BLD-TUNNEL-MODE); at an invalid position the ghost stays a plain entry.
|
||||||
if (!m_ghostValid)
|
if (!m_buildMode.isGhostValid())
|
||||||
{
|
{
|
||||||
|
m_buildMode.setTunnelGhost(BuildingType::TunnelEntry, std::nullopt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,33 +862,34 @@ void GameWorldView::updateTunnelGhost()
|
|||||||
const TunnelLookup lookup = makeTunnelLookup(tunnels);
|
const TunnelLookup lookup = makeTunnelLookup(tunnels);
|
||||||
|
|
||||||
const TunnelCompletion completion =
|
const TunnelCompletion completion =
|
||||||
resolveTunnelCompletion(lookup, m_ghostTile, m_ghostRotation,
|
resolveTunnelCompletion(lookup, m_buildMode.getGhostTile(),
|
||||||
|
m_buildMode.getGhostRotation(),
|
||||||
m_config->world.tunnelMaxDistance_tiles, m_cursorWorldPos);
|
m_config->world.tunnelMaxDistance_tiles, m_cursorWorldPos);
|
||||||
m_tunnelGhostType = completion.resolvedType;
|
m_buildMode.setTunnelGhost(completion.resolvedType, completion.partnerTile);
|
||||||
m_tunnelPartnerTile = completion.partnerTile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::placeAtTile(QPoint tile)
|
void GameWorldView::placeAtTile(QPoint tile)
|
||||||
{
|
{
|
||||||
if (!m_builderType.has_value())
|
if (!m_buildMode.isBuilderMode())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const BuildingType type = effectiveBuilderType();
|
const BuildingType type = m_buildMode.getEffectiveBuilderType();
|
||||||
|
const Rotation rotation = m_buildMode.getGhostRotation();
|
||||||
|
|
||||||
if (!isValidPlacement(type, tile, m_ghostRotation))
|
if (!isValidPlacement(type, tile, rotation))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::optional<BuildingId> rotateTarget =
|
const std::optional<BuildingId> rotateTarget =
|
||||||
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, tile, m_ghostRotation);
|
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, tile, rotation);
|
||||||
if (rotateTarget.has_value())
|
if (rotateTarget.has_value())
|
||||||
{
|
{
|
||||||
std::shared_ptr<RotateInPlaceCommand> command =
|
std::shared_ptr<RotateInPlaceCommand> command =
|
||||||
std::make_shared<RotateInPlaceCommand>();
|
std::make_shared<RotateInPlaceCommand>();
|
||||||
command->id = *rotateTarget;
|
command->id = *rotateTarget;
|
||||||
command->newRotation = m_ghostRotation;
|
command->newRotation = rotation;
|
||||||
enqueueCommand(command);
|
enqueueCommand(command);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -920,12 +905,12 @@ void GameWorldView::placeAtTile(QPoint tile)
|
|||||||
{
|
{
|
||||||
if (!isTileOccupied(m_sim->getFactoryState(), tile) && canAfford(type))
|
if (!isTileOccupied(m_sim->getFactoryState(), tile) && canAfford(type))
|
||||||
{
|
{
|
||||||
enqueuePlaceBuilding(type, tile, m_ghostRotation);
|
enqueuePlaceBuilding(type, tile, rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
enqueuePlaceBuilding(type, tile, m_ghostRotation);
|
enqueuePlaceBuilding(type, tile, rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -983,26 +968,28 @@ void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_beltDragPath = computeBeltDragPath(m_beltDragAnchor, endTile, m_ghostRotation);
|
std::vector<BeltPathTile> path = computeBeltDragPath(
|
||||||
if (forcedEndRotation.has_value() && !m_beltDragPath.empty())
|
m_buildMode.getBeltDragAnchor(), endTile, m_buildMode.getGhostRotation());
|
||||||
|
if (forcedEndRotation.has_value() && !path.empty())
|
||||||
{
|
{
|
||||||
// The end tile points into the target, overriding its incoming-step
|
// The end tile points into the target, overriding its incoming-step
|
||||||
// orientation (REQ-BLD-BELT-DRAG "Snapping to a building").
|
// orientation (REQ-BLD-BELT-DRAG "Snapping to a building").
|
||||||
m_beltDragPath.back().rotation = *forcedEndRotation;
|
path.back().rotation = *forcedEndRotation;
|
||||||
}
|
}
|
||||||
|
m_buildMode.setBeltDragPath(std::move(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath() const
|
std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath() const
|
||||||
{
|
{
|
||||||
std::vector<BeltDragResolved> resolved;
|
std::vector<BeltDragResolved> resolved;
|
||||||
resolved.reserve(m_beltDragPath.size());
|
resolved.reserve(m_buildMode.getBeltDragPath().size());
|
||||||
|
|
||||||
const BuildingDef* def = m_config->buildings.findBuildingDef(BuildingType::Belt);
|
const BuildingDef* def = m_config->buildings.findBuildingDef(BuildingType::Belt);
|
||||||
const int beltCost = (def != nullptr) ? def->cost : 0;
|
const int beltCost = (def != nullptr) ? def->cost : 0;
|
||||||
const int stock = m_sim->getBuildingBlocksStock();
|
const int stock = m_sim->getBuildingBlocksStock();
|
||||||
int spent = 0;
|
int spent = 0;
|
||||||
|
|
||||||
for (const BeltPathTile& entry : m_beltDragPath)
|
for (const BeltPathTile& entry : m_buildMode.getBeltDragPath())
|
||||||
{
|
{
|
||||||
BeltDragResolved item;
|
BeltDragResolved item;
|
||||||
const std::optional<BuildingId> rotateTarget =
|
const std::optional<BuildingId> rotateTarget =
|
||||||
@@ -1040,7 +1027,7 @@ void GameWorldView::applyBeltDragPath()
|
|||||||
for (std::size_t index = 0; index < resolved.size(); ++index)
|
for (std::size_t index = 0; index < resolved.size(); ++index)
|
||||||
{
|
{
|
||||||
const BeltDragResolved& item = resolved[index];
|
const BeltDragResolved& item = resolved[index];
|
||||||
const BeltPathTile& entry = m_beltDragPath[index];
|
const BeltPathTile& entry = m_buildMode.getBeltDragPath()[index];
|
||||||
if (item.action == BeltTileAction::PlaceNew && item.affordable)
|
if (item.action == BeltTileAction::PlaceNew && item.affordable)
|
||||||
{
|
{
|
||||||
enqueuePlaceBuilding(BuildingType::Belt, entry.tile, entry.rotation);
|
enqueuePlaceBuilding(BuildingType::Belt, entry.tile, entry.rotation);
|
||||||
@@ -1927,9 +1914,10 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
|||||||
drawSelectedTunnelConnections(painter, coordinates);
|
drawSelectedTunnelConnections(painter, coordinates);
|
||||||
|
|
||||||
// Builder-mode ghost
|
// Builder-mode ghost
|
||||||
if (m_builderType.has_value())
|
if (m_buildMode.isBuilderMode())
|
||||||
{
|
{
|
||||||
if (*m_builderType == BuildingType::Belt && m_dragging)
|
if (m_buildMode.getBuilderType() == BuildingType::Belt
|
||||||
|
&& m_buildMode.isDraggingBelt())
|
||||||
{
|
{
|
||||||
// Belt drag: a ghost per path tile (REQ-BLD-BELT-DRAG). Rotate-in-place
|
// Belt drag: a ghost per path tile (REQ-BLD-BELT-DRAG). Rotate-in-place
|
||||||
// and affordable new tiles use the belt colors; occupied/invalid tiles
|
// and affordable new tiles use the belt colors; occupied/invalid tiles
|
||||||
@@ -1942,7 +1930,7 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const BeltPathTile& entry = m_beltDragPath[index];
|
const BeltPathTile& entry = m_buildMode.getBeltDragPath()[index];
|
||||||
drawBuildingGhost(painter, coordinates, BuildingType::Belt,
|
drawBuildingGhost(painter, coordinates, BuildingType::Belt,
|
||||||
entry.tile, entry.rotation,
|
entry.tile, entry.rotation,
|
||||||
/*valid*/ item.action != BeltTileAction::Invalid,
|
/*valid*/ item.action != BeltTileAction::Invalid,
|
||||||
@@ -1955,37 +1943,42 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
|||||||
// exit) and, when it would complete an existing tunnel, the matched end
|
// exit) and, when it would complete an existing tunnel, the matched end
|
||||||
// and the tiles between it and the ghost are tinted green
|
// and the tiles between it and the ghost are tinted green
|
||||||
// (REQ-BLD-TUNNEL-MODE).
|
// (REQ-BLD-TUNNEL-MODE).
|
||||||
if (inTunnelMode() && m_ghostValid && m_tunnelPartnerTile.has_value())
|
const QPoint ghostTile = m_buildMode.getGhostTile();
|
||||||
|
const std::optional<QPoint>& partnerTile = m_buildMode.getTunnelPartnerTile();
|
||||||
|
if (m_buildMode.isTunnelMode() && m_buildMode.isGhostValid()
|
||||||
|
&& partnerTile.has_value())
|
||||||
{
|
{
|
||||||
const QColor green = m_visuals->overlays.tunnelPreview;
|
const QColor green = m_visuals->overlays.tunnelPreview;
|
||||||
painter.fillRect(coordinates.tileRect(*m_tunnelPartnerTile), green);
|
painter.fillRect(coordinates.tileRect(*partnerTile), green);
|
||||||
|
|
||||||
// Partner and ghost tile are colinear along the tunnel run; tint the
|
// Partner and ghost tile are colinear along the tunnel run; tint the
|
||||||
// tiles strictly between them.
|
// tiles strictly between them.
|
||||||
const QPoint delta = *m_tunnelPartnerTile - m_ghostTile;
|
const QPoint delta = *partnerTile - ghostTile;
|
||||||
const QPoint step((delta.x() > 0) - (delta.x() < 0),
|
const QPoint step((delta.x() > 0) - (delta.x() < 0),
|
||||||
(delta.y() > 0) - (delta.y() < 0));
|
(delta.y() > 0) - (delta.y() < 0));
|
||||||
for (QPoint t = m_ghostTile + step; t != *m_tunnelPartnerTile; t += step)
|
for (QPoint t = ghostTile + step; t != *partnerTile; t += step)
|
||||||
{
|
{
|
||||||
painter.fillRect(coordinates.tileRect(t), green);
|
painter.fillRect(coordinates.tileRect(t), green);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawBuildingGhost(painter, coordinates, effectiveBuilderType(),
|
drawBuildingGhost(painter, coordinates,
|
||||||
m_ghostTile, m_ghostRotation, m_ghostValid,
|
m_buildMode.getEffectiveBuilderType(),
|
||||||
|
ghostTile, m_buildMode.getGhostRotation(),
|
||||||
|
m_buildMode.isGhostValid(),
|
||||||
/*showPortTargetGlyphs*/ true);
|
/*showPortTargetGlyphs*/ true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blueprint placement ghost
|
// Blueprint placement ghost
|
||||||
if (m_blueprintMode.has_value())
|
if (m_buildMode.isBlueprintMode())
|
||||||
{
|
{
|
||||||
for (const BlueprintBuilding& bb : m_blueprintMode->buildings)
|
for (const BlueprintBuilding& bb : m_buildMode.getBlueprint().buildings)
|
||||||
{
|
{
|
||||||
// Locked building types are omitted from the blueprint (REQ-LOCK-BUILDING,
|
// Locked building types are omitted from the blueprint (REQ-LOCK-BUILDING,
|
||||||
// REQ-LOCK-UI-BLUEPRINT), so they are not ghosted either.
|
// REQ-LOCK-UI-BLUEPRINT), so they are not ghosted either.
|
||||||
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
||||||
const QPoint anchor = m_blueprintGhostTile + bb.offset;
|
const QPoint anchor = m_buildMode.getBlueprintGhostTile() + bb.offset;
|
||||||
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
|
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
|
||||||
drawBuildingGhost(painter, coordinates, bb.type, anchor, bb.rotation,
|
drawBuildingGhost(painter, coordinates, bb.type, anchor, bb.rotation,
|
||||||
valid, /*showPortTargetGlyphs*/ false);
|
valid, /*showPortTargetGlyphs*/ false);
|
||||||
@@ -2006,7 +1999,7 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
|||||||
|
|
||||||
// Deconstruct tint: while dragging a deconstruct box, tint every covered
|
// Deconstruct tint: while dragging a deconstruct box, tint every covered
|
||||||
// building/site (REQ-BLD-DECONSTRUCT-BOX); otherwise tint the hovered one.
|
// building/site (REQ-BLD-DECONSTRUCT-BOX); otherwise tint the hovered one.
|
||||||
if (m_deconstructMode && m_boxSelecting)
|
if (m_buildMode.isDeconstructMode() && m_boxSelecting)
|
||||||
{
|
{
|
||||||
for (BuildingId id : buildingsInBox(m_boxStartTile, m_boxCurrentTile))
|
for (BuildingId id : buildingsInBox(m_boxStartTile, m_boxCurrentTile))
|
||||||
{
|
{
|
||||||
@@ -2026,9 +2019,11 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (m_deconstructMode && m_deconstructHoverBuildingId.has_value())
|
else if (m_buildMode.isDeconstructMode()
|
||||||
|
&& m_buildMode.getDeconstructHoverBuildingId().has_value())
|
||||||
{
|
{
|
||||||
const Building* b = findBuilding(m_sim->getFactoryState(), *m_deconstructHoverBuildingId);
|
const Building* b = findBuilding(m_sim->getFactoryState(),
|
||||||
|
*m_buildMode.getDeconstructHoverBuildingId());
|
||||||
if (b)
|
if (b)
|
||||||
{
|
{
|
||||||
for (const QPoint& cell : b->bodyCells)
|
for (const QPoint& cell : b->bodyCells)
|
||||||
@@ -2151,7 +2146,7 @@ void GameWorldView::drawPauseBorder(QPainter& painter)
|
|||||||
|
|
||||||
void GameWorldView::drawDeconstructBorder(QPainter& painter)
|
void GameWorldView::drawDeconstructBorder(QPainter& painter)
|
||||||
{
|
{
|
||||||
if (!m_deconstructMode) { return; }
|
if (!m_buildMode.isDeconstructMode()) { return; }
|
||||||
// Reuse the deconstruct overlay color (with its configured alpha) as the edge color.
|
// Reuse the deconstruct overlay color (with its configured alpha) as the edge color.
|
||||||
drawVignetteBorder(painter, m_visuals->overlays.deconstructTint);
|
drawVignetteBorder(painter, m_visuals->overlays.deconstructTint);
|
||||||
}
|
}
|
||||||
@@ -2322,22 +2317,16 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
{
|
{
|
||||||
if (event->button() == Qt::RightButton)
|
if (event->button() == Qt::RightButton)
|
||||||
{
|
{
|
||||||
if (m_builderType.has_value())
|
if (m_buildMode.isBuilderMode() && m_buildMode.isDraggingBelt())
|
||||||
{
|
|
||||||
if (m_dragging)
|
|
||||||
{
|
{
|
||||||
// Cancel the in-progress belt drag without placing anything;
|
// Cancel the in-progress belt drag without placing anything;
|
||||||
// stay in belt builder mode (REQ-BLD-BELT-DRAG).
|
// stay in belt builder mode (REQ-BLD-BELT-DRAG).
|
||||||
m_dragging = false;
|
m_buildMode.cancelBeltDrag();
|
||||||
m_beltDragPath.clear();
|
|
||||||
}
|
}
|
||||||
else
|
else if (m_buildMode.getMode() != BuildMode::None)
|
||||||
{
|
{
|
||||||
exitBuilderMode();
|
m_buildMode.exitCurrentMode();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
|
|
||||||
else if (m_deconstructMode) { toggleDeconstructMode(); }
|
|
||||||
else if (event->modifiers() & Qt::ShiftModifier)
|
else if (event->modifiers() & Qt::ShiftModifier)
|
||||||
{
|
{
|
||||||
// Shift + right-click copies a building's settings, but only in the
|
// Shift + right-click copies a building's settings, but only in the
|
||||||
@@ -2353,15 +2342,13 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
|
|
||||||
const QPoint tile = coordinates.widgetToTile(event->pos());
|
const QPoint tile = coordinates.widgetToTile(event->pos());
|
||||||
|
|
||||||
if (m_builderType.has_value())
|
if (m_buildMode.isBuilderMode())
|
||||||
{
|
{
|
||||||
const BuildingType type = *m_builderType;
|
if (m_buildMode.getBuilderType() == BuildingType::Belt)
|
||||||
if (type == BuildingType::Belt)
|
|
||||||
{
|
{
|
||||||
// Deferred placement: start the drag and show the path ghost; nothing
|
// Deferred placement: start the drag and show the path ghost; nothing
|
||||||
// is placed until release (REQ-BLD-BELT-DRAG).
|
// is placed until release (REQ-BLD-BELT-DRAG).
|
||||||
m_dragging = true;
|
m_buildMode.beginBeltDrag(tile);
|
||||||
m_beltDragAnchor = tile;
|
|
||||||
m_cursorWorldPos = coordinates.widgetToWorld(event->pos());
|
m_cursorWorldPos = coordinates.widgetToWorld(event->pos());
|
||||||
recomputeBeltDragPath(tile);
|
recomputeBeltDragPath(tile);
|
||||||
}
|
}
|
||||||
@@ -2370,11 +2357,11 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
placeAtTile(tile);
|
placeAtTile(tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (m_blueprintMode.has_value())
|
else if (m_buildMode.isBlueprintMode())
|
||||||
{
|
{
|
||||||
placeBlueprintAtTile(tile);
|
placeBlueprintAtTile(tile);
|
||||||
}
|
}
|
||||||
else if (m_deconstructMode)
|
else if (m_buildMode.isDeconstructMode())
|
||||||
{
|
{
|
||||||
// Start a deconstruct box drag; a plain click resolves as a 1x1 box on
|
// Start a deconstruct box drag; a plain click resolves as a 1x1 box on
|
||||||
// release (REQ-BLD-DECONSTRUCT-CLICK, REQ-BLD-DECONSTRUCT-BOX).
|
// release (REQ-BLD-DECONSTRUCT-CLICK, REQ-BLD-DECONSTRUCT-BOX).
|
||||||
@@ -2478,32 +2465,34 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
|||||||
const QPoint tile = coordinates.widgetToTile(event->pos());
|
const QPoint tile = coordinates.widgetToTile(event->pos());
|
||||||
m_cursorWorldPos = coordinates.widgetToWorld(event->pos());
|
m_cursorWorldPos = coordinates.widgetToWorld(event->pos());
|
||||||
|
|
||||||
if (m_builderType.has_value())
|
if (m_buildMode.isBuilderMode())
|
||||||
{
|
{
|
||||||
m_ghostTile = tile;
|
m_buildMode.setGhostTile(tile);
|
||||||
m_ghostValid = isValidPlacement(*m_builderType, tile, m_ghostRotation);
|
m_buildMode.setGhostValidity(
|
||||||
|
isValidPlacement(m_buildMode.getBuilderType(), tile,
|
||||||
|
m_buildMode.getGhostRotation()));
|
||||||
|
|
||||||
if (inTunnelMode())
|
if (m_buildMode.isTunnelMode())
|
||||||
{
|
{
|
||||||
// Resolve entry vs exit and the completion partner for the new hover
|
// Resolve entry vs exit and the completion partner for the new hover
|
||||||
// position and sub-tile cursor (REQ-BLD-TUNNEL-MODE).
|
// position and sub-tile cursor (REQ-BLD-TUNNEL-MODE).
|
||||||
updateTunnelGhost();
|
updateTunnelGhost();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_dragging)
|
if (m_buildMode.isDraggingBelt())
|
||||||
{
|
{
|
||||||
// Belt drag: update the previewed path; placement happens on release
|
// Belt drag: update the previewed path; placement happens on release
|
||||||
// (REQ-BLD-BELT-DRAG).
|
// (REQ-BLD-BELT-DRAG).
|
||||||
recomputeBeltDragPath(tile);
|
recomputeBeltDragPath(tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (m_blueprintMode.has_value())
|
else if (m_buildMode.isBlueprintMode())
|
||||||
{
|
{
|
||||||
m_blueprintGhostTile = tile;
|
m_buildMode.setBlueprintGhostTile(tile);
|
||||||
}
|
}
|
||||||
else if (m_deconstructMode)
|
else if (m_buildMode.isDeconstructMode())
|
||||||
{
|
{
|
||||||
m_deconstructHoverBuildingId = buildingAtTile(tile);
|
m_buildMode.setDeconstructHoverBuildingId(buildingAtTile(tile));
|
||||||
if (m_boxSelecting) { m_boxCurrentTile = tile; }
|
if (m_boxSelecting) { m_boxCurrentTile = tile; }
|
||||||
}
|
}
|
||||||
else if (m_boxSelecting)
|
else if (m_boxSelecting)
|
||||||
@@ -2516,13 +2505,12 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
{
|
{
|
||||||
if (event->button() != Qt::LeftButton) { return; }
|
if (event->button() != Qt::LeftButton) { return; }
|
||||||
|
|
||||||
if (m_dragging)
|
if (m_buildMode.isDraggingBelt())
|
||||||
{
|
{
|
||||||
// Apply the previewed belt path now that the button is released
|
// Apply the previewed belt path now that the button is released
|
||||||
// (REQ-BLD-BELT-DRAG).
|
// (REQ-BLD-BELT-DRAG).
|
||||||
applyBeltDragPath();
|
applyBeltDragPath();
|
||||||
m_dragging = false;
|
m_buildMode.cancelBeltDrag();
|
||||||
m_beltDragPath.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_boxSelecting)
|
if (m_boxSelecting)
|
||||||
@@ -2532,7 +2520,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
const std::vector<BuildingId> boxIds =
|
const std::vector<BuildingId> boxIds =
|
||||||
buildingsInBox(m_boxStartTile, m_boxCurrentTile);
|
buildingsInBox(m_boxStartTile, m_boxCurrentTile);
|
||||||
|
|
||||||
if (m_deconstructMode)
|
if (m_buildMode.isDeconstructMode())
|
||||||
{
|
{
|
||||||
const FactoryState& factory = m_sim->getFactoryState();
|
const FactoryState& factory = m_sim->getFactoryState();
|
||||||
|
|
||||||
@@ -2590,7 +2578,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_deconstructHoverBuildingId = std::nullopt;
|
m_buildMode.setDeconstructHoverBuildingId(std::nullopt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2602,39 +2590,26 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
// Methods (formerly slots)
|
// Methods (formerly slots)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
void GameWorldView::toggleDeconstructMode()
|
|
||||||
{
|
|
||||||
if (m_deconstructMode)
|
|
||||||
{
|
|
||||||
m_deconstructMode = false;
|
|
||||||
m_deconstructHoverBuildingId = std::nullopt;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (m_builderType.has_value()) { exitBuilderMode(); }
|
|
||||||
if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
|
|
||||||
m_deconstructMode = true;
|
|
||||||
}
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<DeconstructModeChangedEvent>(m_deconstructMode));
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameWorldView::rotateGhost(bool clockwise)
|
void GameWorldView::rotateGhost(bool clockwise)
|
||||||
{
|
{
|
||||||
if (m_builderType.has_value())
|
if (m_buildMode.isBuilderMode())
|
||||||
{
|
{
|
||||||
m_ghostRotation = clockwise ? rotateClockwise(m_ghostRotation)
|
m_buildMode.rotateGhost(clockwise);
|
||||||
: rotateCounterClockwise(m_ghostRotation);
|
m_buildMode.setGhostValidity(
|
||||||
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
|
isValidPlacement(m_buildMode.getBuilderType(), m_buildMode.getGhostTile(),
|
||||||
|
m_buildMode.getGhostRotation()));
|
||||||
// A new facing changes which tunnels the ghost could complete (REQ-BLD-TUNNEL-MODE).
|
// A new facing changes which tunnels the ghost could complete (REQ-BLD-TUNNEL-MODE).
|
||||||
if (inTunnelMode()) { updateTunnelGhost(); }
|
if (m_buildMode.isTunnelMode()) { updateTunnelGhost(); }
|
||||||
// Rotating during a belt drag re-picks the path's primary axis immediately,
|
// Rotating during a belt drag re-picks the path's primary axis immediately,
|
||||||
// without waiting for the next mouse move (REQ-BLD-BELT-DRAG).
|
// without waiting for the next mouse move (REQ-BLD-BELT-DRAG).
|
||||||
if (m_dragging) { recomputeBeltDragPath(m_ghostTile); }
|
if (m_buildMode.isDraggingBelt())
|
||||||
}
|
|
||||||
else if (m_blueprintMode.has_value())
|
|
||||||
{
|
{
|
||||||
for (BlueprintBuilding& bb : m_blueprintMode->buildings)
|
recomputeBeltDragPath(m_buildMode.getGhostTile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_buildMode.isBlueprintMode())
|
||||||
|
{
|
||||||
|
for (BlueprintBuilding& bb : m_buildMode.getMutableBlueprint().buildings)
|
||||||
{
|
{
|
||||||
const BuildingDef* def = m_config->buildings.findBuildingDef(bb.type);
|
const BuildingDef* def = m_config->buildings.findBuildingDef(bb.type);
|
||||||
if (!def) { continue; }
|
if (!def) { continue; }
|
||||||
@@ -2740,45 +2715,6 @@ void GameWorldView::pasteConfigTo(BuildingId id)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::enterBuilderMode(BuildingType type)
|
|
||||||
{
|
|
||||||
m_builderType = type;
|
|
||||||
m_ghostRotation = Rotation::East;
|
|
||||||
m_ghostValid = false;
|
|
||||||
m_tunnelGhostType = BuildingType::TunnelEntry;
|
|
||||||
m_tunnelPartnerTile.reset();
|
|
||||||
m_deconstructMode = false;
|
|
||||||
m_blueprintMode.reset();
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<DeconstructModeChangedEvent>(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameWorldView::enterBlueprintMode(Blueprint blueprint)
|
|
||||||
{
|
|
||||||
if (m_builderType.has_value()) { exitBuilderMode(); }
|
|
||||||
m_deconstructMode = false;
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<DeconstructModeChangedEvent>(false));
|
|
||||||
m_blueprintGhostTile = m_ghostTile;
|
|
||||||
m_blueprintMode = std::move(blueprint);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameWorldView::exitBlueprintMode()
|
|
||||||
{
|
|
||||||
m_blueprintMode.reset();
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<BlueprintModeExitedEvent>());
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameWorldView::exitBuilderMode()
|
|
||||||
{
|
|
||||||
m_builderType.reset();
|
|
||||||
m_beltDragPath.clear();
|
|
||||||
m_dragging = false;
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<BuilderModeExitedEvent>());
|
|
||||||
}
|
|
||||||
|
|
||||||
double GameWorldView::getGameSpeed() const
|
double GameWorldView::getGameSpeed() const
|
||||||
{
|
{
|
||||||
return m_gameSpeedMultiplier;
|
return m_gameSpeedMultiplier;
|
||||||
@@ -2803,16 +2739,11 @@ void GameWorldView::setGameSpeed(double multiplier)
|
|||||||
|
|
||||||
void GameWorldView::resetForNewGame()
|
void GameWorldView::resetForNewGame()
|
||||||
{
|
{
|
||||||
exitBuilderMode();
|
// Leaves whichever mode was active, announcing that one exit rather than all
|
||||||
exitBlueprintMode();
|
// three; a mode that was not active has nothing to announce.
|
||||||
|
m_buildMode.exitCurrentMode();
|
||||||
m_activeBeams.clear();
|
m_activeBeams.clear();
|
||||||
m_schematicChoiceShown = false;
|
m_schematicChoiceShown = false;
|
||||||
m_ghostRotation = Rotation::East;
|
|
||||||
m_ghostValid = false;
|
|
||||||
m_deconstructMode = false;
|
|
||||||
m_deconstructHoverBuildingId = std::nullopt;
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<DeconstructModeChangedEvent>(false));
|
|
||||||
m_selection.clearAll();
|
m_selection.clearAll();
|
||||||
m_copiedConfig = std::nullopt;
|
m_copiedConfig = std::nullopt;
|
||||||
m_copyConfigFlashes.clear();
|
m_copyConfigFlashes.clear();
|
||||||
@@ -2879,27 +2810,27 @@ void GameWorldView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
|
|||||||
|
|
||||||
void GameWorldView::handleEvent(std::shared_ptr<const BuildingTypeSelectedEvent> event)
|
void GameWorldView::handleEvent(std::shared_ptr<const BuildingTypeSelectedEvent> event)
|
||||||
{
|
{
|
||||||
enterBuilderMode(event->type);
|
m_buildMode.enterBuilderMode(event->type);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::handleEvent(std::shared_ptr<const ExitBuilderModeRequestedEvent> /*event*/)
|
void GameWorldView::handleEvent(std::shared_ptr<const ExitBuilderModeRequestedEvent> /*event*/)
|
||||||
{
|
{
|
||||||
exitBuilderMode();
|
m_buildMode.exitBuilderMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::handleEvent(std::shared_ptr<const DeconstructModeToggleRequestedEvent> /*event*/)
|
void GameWorldView::handleEvent(std::shared_ptr<const DeconstructModeToggleRequestedEvent> /*event*/)
|
||||||
{
|
{
|
||||||
toggleDeconstructMode();
|
m_buildMode.toggleDeconstructMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event)
|
void GameWorldView::handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event)
|
||||||
{
|
{
|
||||||
enterBlueprintMode(event->blueprint);
|
m_buildMode.enterBlueprintMode(event->blueprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::handleEvent(std::shared_ptr<const ExitBlueprintModeRequestedEvent> /*event*/)
|
void GameWorldView::handleEvent(std::shared_ptr<const ExitBlueprintModeRequestedEvent> /*event*/)
|
||||||
{
|
{
|
||||||
exitBlueprintMode();
|
m_buildMode.exitBlueprintMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::handleEvent(std::shared_ptr<const SpeedChangeRequestedEvent> event)
|
void GameWorldView::handleEvent(std::shared_ptr<const SpeedChangeRequestedEvent> event)
|
||||||
@@ -2939,9 +2870,10 @@ void GameWorldView::handleEvent(std::shared_ptr<const ModeCancelRequestedEvent>
|
|||||||
{
|
{
|
||||||
// One key backs out of whichever mode is active, and enters deconstruct mode
|
// One key backs out of whichever mode is active, and enters deconstruct mode
|
||||||
// when none is (REQ-UI-HOTKEYS).
|
// when none is (REQ-UI-HOTKEYS).
|
||||||
if (m_builderType.has_value()) { exitBuilderMode(); }
|
// One key backs out of whichever mode is active, and enters deconstruct mode
|
||||||
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
|
// when none is (REQ-UI-HOTKEYS).
|
||||||
else { toggleDeconstructMode(); }
|
if (m_buildMode.getMode() == BuildMode::None) { m_buildMode.toggleDeconstructMode(); }
|
||||||
|
else { m_buildMode.exitCurrentMode(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::handleEvent(std::shared_ptr<const DebugDrawToggleRequestedEvent> /*event*/)
|
void GameWorldView::handleEvent(std::shared_ptr<const DebugDrawToggleRequestedEvent> /*event*/)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "Blueprint.h"
|
#include "Blueprint.h"
|
||||||
|
#include "BuildModeController.h"
|
||||||
#include "BuildingConfig.h"
|
#include "BuildingConfig.h"
|
||||||
#include "BlueprintModeExitedEvent.h"
|
#include "BlueprintModeExitedEvent.h"
|
||||||
#include "BlueprintPlacementRequestedEvent.h"
|
#include "BlueprintPlacementRequestedEvent.h"
|
||||||
@@ -262,14 +263,9 @@ private:
|
|||||||
void stepSpeed(int delta);
|
void stepSpeed(int delta);
|
||||||
void placeAtTile(QPoint tile);
|
void placeAtTile(QPoint tile);
|
||||||
|
|
||||||
// Unified tunnel build mode (REQ-BLD-TUNNEL-MODE). Active while the builder type
|
// Re-resolves the tunnel ghost type and completion partner from the current
|
||||||
// is TunnelEntry; the ghost then resolves to an entry or exit by hovered position.
|
// ghost tile, rotation, and sub-tile cursor position, storing both on the build
|
||||||
bool inTunnelMode() const;
|
// mode controller. Only meaningful in tunnel mode (REQ-BLD-TUNNEL-MODE).
|
||||||
// The building type the ghost currently represents: the position-resolved tunnel
|
|
||||||
// type in tunnel mode, otherwise the plain builder type.
|
|
||||||
BuildingType effectiveBuilderType() const;
|
|
||||||
// Recomputes m_tunnelGhostType and m_tunnelPartnerTile from the current ghost
|
|
||||||
// tile, rotation, and sub-tile cursor position. Only meaningful in tunnel mode.
|
|
||||||
void updateTunnelGhost();
|
void updateTunnelGhost();
|
||||||
// Indexes every tunnel entry/exit — built or still a construction site — by its
|
// Indexes every tunnel entry/exit — built or still a construction site — by its
|
||||||
// single-cell tile. Shared by the placement preview and the selection highlight.
|
// single-cell tile. Shared by the placement preview and the selection highlight.
|
||||||
@@ -291,8 +287,8 @@ private:
|
|||||||
bool affordable; // meaningful only for PlaceNew
|
bool affordable; // meaningful only for PlaceNew
|
||||||
std::optional<BuildingId> rotateId; // set only for RotateInPlace
|
std::optional<BuildingId> rotateId; // set only for RotateInPlace
|
||||||
};
|
};
|
||||||
// Recomputes m_beltDragPath from m_beltDragAnchor to cursorTile using the
|
// Recomputes the drag path from its anchor to cursorTile using the current ghost
|
||||||
// current ghost orientation.
|
// orientation, and stores it on the build mode controller.
|
||||||
void recomputeBeltDragPath(QPoint cursorTile);
|
void recomputeBeltDragPath(QPoint cursorTile);
|
||||||
// Classifies each path tile against the current sim state, applying cumulative
|
// Classifies each path tile against the current sim state, applying cumulative
|
||||||
// affordability to the PlaceNew tiles.
|
// affordability to the PlaceNew tiles.
|
||||||
@@ -306,11 +302,9 @@ private:
|
|||||||
void copyConfigFrom(BuildingId id);
|
void copyConfigFrom(BuildingId id);
|
||||||
void pasteConfigTo(BuildingId id);
|
void pasteConfigTo(BuildingId id);
|
||||||
|
|
||||||
void enterBuilderMode(BuildingType type);
|
// Turns the ghost and refreshes everything that depends on its facing: placement
|
||||||
void exitBuilderMode();
|
// validity, the tunnel completion match, and an in-progress belt drag's path.
|
||||||
void enterBlueprintMode(Blueprint blueprint);
|
// The mode transitions themselves live on m_buildMode.
|
||||||
void exitBlueprintMode();
|
|
||||||
void toggleDeconstructMode();
|
|
||||||
void rotateGhost(bool clockwise);
|
void rotateGhost(bool clockwise);
|
||||||
|
|
||||||
struct ActiveBeam
|
struct ActiveBeam
|
||||||
@@ -364,28 +358,15 @@ private:
|
|||||||
|
|
||||||
std::vector<ActiveBeam> m_activeBeams;
|
std::vector<ActiveBeam> m_activeBeams;
|
||||||
|
|
||||||
std::optional<BuildingType> m_builderType;
|
// The active build mode (builder / blueprint / deconstruct, or none) and the
|
||||||
Rotation m_ghostRotation;
|
// ghost, tunnel and belt-drag state belonging to it. Owns the transitions
|
||||||
QPoint m_ghostTile;
|
// between them; this widget supplies the parts that need the simulation.
|
||||||
bool m_ghostValid;
|
BuildModeController m_buildMode;
|
||||||
// Unified tunnel build mode (REQ-BLD-TUNNEL-MODE): while m_builderType is
|
|
||||||
// TunnelEntry the ghost resolves to an entry or an exit based on the hovered
|
|
||||||
// position; m_tunnelPartnerTile is the existing tunnel it would complete (drawn
|
|
||||||
// as the green connection preview), or unset when there is no completion match.
|
|
||||||
BuildingType m_tunnelGhostType = BuildingType::TunnelEntry;
|
|
||||||
std::optional<QPoint> m_tunnelPartnerTile;
|
|
||||||
// Deferred belt drag placement (REQ-BLD-BELT-DRAG): while dragging, the
|
|
||||||
// rectilinear anchor->cursor path is recomputed on each move and only applied
|
|
||||||
// on release. Empty unless a belt drag is in progress.
|
|
||||||
std::vector<BeltPathTile> m_beltDragPath;
|
|
||||||
QPoint m_beltDragAnchor;
|
|
||||||
// Last known cursor position in world (tile) units; used to pick the belt-drag
|
|
||||||
// end tile closest to the cursor when snapping to a building (REQ-BLD-BELT-DRAG).
|
|
||||||
QVector2D m_cursorWorldPos;
|
|
||||||
bool m_dragging;
|
|
||||||
|
|
||||||
std::optional<Blueprint> m_blueprintMode;
|
// Last known cursor position in world (tile) units; used to pick the belt-drag
|
||||||
QPoint m_blueprintGhostTile;
|
// end tile closest to the cursor when snapping to a building (REQ-BLD-BELT-DRAG)
|
||||||
|
// and to resolve the tunnel ghost sub-tile (REQ-BLD-TUNNEL-MODE).
|
||||||
|
QVector2D m_cursorWorldPos;
|
||||||
|
|
||||||
// Temporary cache for the copy-settings gesture (REQ-BLD-COPY-CONFIG); held
|
// Temporary cache for the copy-settings gesture (REQ-BLD-COPY-CONFIG); held
|
||||||
// only while Shift is down and cleared on Shift release.
|
// only while Shift is down and cleared on Shift release.
|
||||||
@@ -403,8 +384,6 @@ private:
|
|||||||
std::vector<CopyConfigFlash> m_copyConfigFlashes;
|
std::vector<CopyConfigFlash> m_copyConfigFlashes;
|
||||||
static constexpr qint64 kCopyFlashDurationMs = 300;
|
static constexpr qint64 kCopyFlashDurationMs = 300;
|
||||||
|
|
||||||
bool m_deconstructMode;
|
|
||||||
std::optional<BuildingId> m_deconstructHoverBuildingId;
|
|
||||||
bool m_debugDraw;
|
bool m_debugDraw;
|
||||||
|
|
||||||
// Owns the selection across all three categories and the rules for moving
|
// Owns the selection across all three categories and the rules for moving
|
||||||
|
|||||||
Reference in New Issue
Block a user