extract BuildModeController, fixing a silent blueprint exit
This commit is contained in:
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
|
||||
WorldCameraTest.cpp
|
||||
SelectionControllerTest.cpp
|
||||
BuildModeControllerTest.cpp
|
||||
BuildingTest.cpp
|
||||
BuildingConfigTest.cpp
|
||||
ShipTest.cpp
|
||||
|
||||
Reference in New Issue
Block a user