From 9692df9101b1e5cdc7eed3194f895e9f921ae7e9 Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Fri, 21 Aug 2026 22:23:01 -0400 Subject: [PATCH] fix(flight): show boundary warning only while flying --- .../us-004-unlock-and-control-base-flight.md | 2 +- .../dmg/spigotbase/BaseFlightController.java | 2 +- .../spigotbase/BaseFlightControllerTest.java | 93 +++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/test/java/games/dmg/spigotbase/BaseFlightControllerTest.java diff --git a/design/user-stories/us-004-unlock-and-control-base-flight.md b/design/user-stories/us-004-unlock-and-control-base-flight.md index 38ceb97..6be795a 100644 --- a/design/user-stories/us-004-unlock-and-control-base-flight.md +++ b/design/user-stories/us-004-unlock-and-control-base-flight.md @@ -21,7 +21,7 @@ As a **player with Base I**, I want to unlock controlled flight around my base s - [x] Flight II expands the vertical range to 100 blocks below and above base Y by default. - [x] Flight III expands the vertical range to the world's minimum and maximum build heights. - [x] A configurable five-block horizontal warning buffer extends beyond the current base radius. -- [x] Plugin-granted flight remains active in the warning buffer and displays prominent on-screen notice that the player is leaving the base. +- [x] Plugin-granted flight remains active in the warning buffer, and a prominent on-screen notice that the player is leaving the base appears only while the player is actively flying. - [x] Passing beyond the warning buffer removes only flight granted by this plugin. - [x] Flight is not granted outside the unlocked vertical range. - [x] `/basesettings flight enable` enables unlocked flight idempotently, and `/basesettings flight disable` disables it idempotently. diff --git a/src/main/java/games/dmg/spigotbase/BaseFlightController.java b/src/main/java/games/dmg/spigotbase/BaseFlightController.java index 3783bd9..a831e8f 100644 --- a/src/main/java/games/dmg/spigotbase/BaseFlightController.java +++ b/src/main/java/games/dmg/spigotbase/BaseFlightController.java @@ -112,7 +112,7 @@ final class BaseFlightController implements Runnable { player.setAllowFlight(true); grantedFlight.add(player.getUniqueId()); } - if (distanceSquared > (double) radius * radius) { + if (distanceSquared > (double) radius * radius && player.isFlying()) { if (warned.add(player.getUniqueId())) { player.sendTitle( ChatColor.RED + "Leaving Your Base", diff --git a/src/test/java/games/dmg/spigotbase/BaseFlightControllerTest.java b/src/test/java/games/dmg/spigotbase/BaseFlightControllerTest.java new file mode 100644 index 0000000..839bf47 --- /dev/null +++ b/src/test/java/games/dmg/spigotbase/BaseFlightControllerTest.java @@ -0,0 +1,93 @@ +package games.dmg.spigotbase; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.bukkit.Location; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +final class BaseFlightControllerTest { + private final UUID playerId = UUID.randomUUID(); + private final UUID worldId = UUID.randomUUID(); + private final Server server = mock(Server.class); + private final BaseStateManager stateManager = mock(BaseStateManager.class); + private final Player player = mock(Player.class); + private final PlayerState state = PlayerState.newPlayer(playerId, "Alex") + .withGrassAndDirtProgress(250, 1) + .withBase(new BaseLocation(worldId, "world", 0, 64, 0, 0, 0), Instant.EPOCH) + .withFlightLevel(1, true); + private final BaseFlightController controller = new BaseFlightController( + server, + stateManager, + new SecondaryProgressionService(PluginSettings.from(Map.of())), + new BaseBoundsService(PluginSettings.from(Map.of())), + new PluginSettingsProvider(PluginSettings.from(Map.of())) + ); + + @BeforeEach + void setUp() { + World world = mock(World.class); + Location location = mock(Location.class); + PlayerInventory inventory = mock(PlayerInventory.class); + + doReturn(List.of(player)).when(server).getOnlinePlayers(); + when(player.getUniqueId()).thenReturn(playerId); + when(player.getName()).thenReturn("Alex"); + when(player.getGameMode()).thenReturn(org.bukkit.GameMode.SURVIVAL); + when(player.getInventory()).thenReturn(inventory); + when(inventory.getStorageContents()).thenReturn(new ItemStack[0]); + when(stateManager.player(playerId, "Alex")).thenReturn(state); + when(player.getWorld()).thenReturn(world); + when(world.getUID()).thenReturn(worldId); + when(player.getLocation()).thenReturn(location); + when(location.getX()).thenReturn(12.5); + when(location.getZ()).thenReturn(0.5); + when(location.getBlockY()).thenReturn(64); + when(player.getAllowFlight()).thenReturn(true); + } + + @Test + void walkingInWarningBufferDoesNotShowLeavingBaseWarning() { + when(player.isFlying()).thenReturn(false); + + controller.run(); + + verify(player, never()).sendTitle(anyString(), anyString(), anyInt(), anyInt(), anyInt()); + } + + @Test + void flyingInWarningBufferShowsLeavingBaseWarning() { + when(player.isFlying()).thenReturn(true); + + controller.run(); + + verify(player).sendTitle(anyString(), anyString(), anyInt(), anyInt(), anyInt()); + } + + @Test + void warningCanAppearAgainAfterPlayerStopsAndResumesFlyingInBuffer() { + when(player.isFlying()).thenReturn(true, false, true); + + controller.run(); + controller.run(); + controller.run(); + + verify(player, times(2)).sendTitle(anyString(), anyString(), anyInt(), anyInt(), anyInt()); + } +}