#include "catch.hpp" #include #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 { public: ModeEventSpy() { registerForEvents(); } ~ModeEventSpy() { unregisterForEvents(); } int builderExits = 0; int blueprintExits = 0; int deconstructChanges = 0; bool lastDeconstructActive = false; private: void handleEvent(std::shared_ptr /*event*/) override { ++builderExits; } void handleEvent(std::shared_ptr /*event*/) override { ++blueprintExits; } void handleEvent(std::shared_ptr 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()); }