302 lines
10 KiB
C++
302 lines
10 KiB
C++
#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 library kept tracking an active blueprint 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());
|
|
}
|