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}/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).
|
||||||
@@ -2483,32 +2470,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)
|
||||||
@@ -2521,13 +2510,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)
|
||||||
@@ -2537,7 +2525,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();
|
||||||
|
|
||||||
@@ -2595,7 +2583,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_deconstructHoverBuildingId = std::nullopt;
|
m_buildMode.setDeconstructHoverBuildingId(std::nullopt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2607,39 +2595,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; }
|
||||||
@@ -2745,45 +2720,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;
|
||||||
@@ -2808,16 +2744,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();
|
||||||
@@ -2884,27 +2815,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)
|
||||||
@@ -2944,9 +2875,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"
|
||||||
@@ -263,14 +264,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.
|
||||||
@@ -292,8 +288,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.
|
||||||
@@ -307,11 +303,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
|
||||||
@@ -365,28 +359,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.
|
||||||
@@ -404,8 +385,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