diff --git a/design/log.md b/design/log.md index 21253ba..5778a1e 100644 --- a/design/log.md +++ b/design/log.md @@ -143,3 +143,9 @@ description: Chronological record of material decisions affecting the Spigot Bas - Reused safe player destinations and server-spawn fallback behavior while excluding items, projectiles, vehicles, and other non-living entities. - Applied the existing two-second portal cooldown to mobs to prevent immediate repeated transport. - Verified the feature and Java 25 plugin artifact with `./gradlew clean check jar`. + +## 2026-08-28 — Safe Pocket Base fall returns + +- Cleared accumulated fall distance whenever portal or void-return travel teleports an entity, preventing carried-over fall damage at the destination. +- Added regression coverage for players falling out of a Pocket Base and returning to the server spawn. +- Verified the fix and plugin artifact with `./gradlew clean check jar`. diff --git a/design/user-stories/us-016-build-and-use-pocket-base-portals.md b/design/user-stories/us-016-build-and-use-pocket-base-portals.md index 382f459..6b91d3d 100644 --- a/design/user-stories/us-016-build-and-use-pocket-base-portals.md +++ b/design/user-stories/us-016-build-and-use-pocket-base-portals.md @@ -33,6 +33,7 @@ As a **Pocket Base owner**, I want to connect my normal base to my Pocket Base w - [x] Breaking the active return frame disables return travel through it. - [x] Return travel falls back to the server spawn when the owner has no valid normal-world entrance. - [x] Falling below Y -64 performs the same safe return without void damage. +- [x] Portal and void-return travel clears accumulated fall distance so players do not take carried-over fall damage after landing at the destination. - [x] Players who disconnect inside a Pocket Base remain there when they reconnect. - [x] Moving the normal base deactivates its entrance while preserving the Pocket Base world and its contents. - [x] Portal ownership and the active entrance location persist through restarts. diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseController.java b/src/main/java/games/dmg/spigotbase/PocketBaseController.java index 429c7ba..84806c9 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseController.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseController.java @@ -289,6 +289,7 @@ final class PocketBaseController implements Listener, Runnable { private void teleport(Entity entity, Location destination) { cooldownUntil.put(entity.getUniqueId(), System.nanoTime() + PORTAL_COOLDOWN_NANOS); + entity.setFallDistance(0.0F); entity.teleport(destination, PlayerTeleportEvent.TeleportCause.PLUGIN); } diff --git a/src/test/java/games/dmg/spigotbase/PocketBaseControllerTest.java b/src/test/java/games/dmg/spigotbase/PocketBaseControllerTest.java index eafa49c..f2ca5c1 100644 --- a/src/test/java/games/dmg/spigotbase/PocketBaseControllerTest.java +++ b/src/test/java/games/dmg/spigotbase/PocketBaseControllerTest.java @@ -26,6 +26,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.block.Action; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.util.BoundingBox; @@ -310,6 +311,37 @@ final class PocketBaseControllerTest { ); } + @Test + void voidReturnClearsAccumulatedPlayerFallDistance() { + UUID ownerId = UUID.randomUUID(); + UUID pocketWorldId = UUID.randomUUID(); + World pocketWorld = mock(World.class); + World fallbackWorld = mock(World.class); + Location destination = new Location(pocketWorld, 0.5, -65.0, 0.5); + Location spawn = new Location(fallbackWorld, 10.5, 70.0, 10.5); + Player player = mock(Player.class); + PlayerMoveEvent event = mock(PlayerMoveEvent.class); + PocketBaseManager pocketBases = mock(PocketBaseManager.class); + Server server = mock(Server.class); + + when(pocketWorld.getUID()).thenReturn(pocketWorldId); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + when(event.getPlayer()).thenReturn(player); + when(event.getTo()).thenReturn(destination); + when(server.getWorlds()).thenReturn(List.of(fallbackWorld)); + when(fallbackWorld.getSpawnLocation()).thenReturn(spawn); + when(pocketBases.ownerForPocketWorld(pocketWorldId)).thenReturn(Optional.of(ownerId)); + when(pocketBases.state(ownerId)).thenReturn( + new PocketBaseState(ownerId, 1, Optional.empty()) + ); + PocketBaseController controller = controller(server, pocketBases); + + controller.onMove(event); + + verify(player).setFallDistance(0.0F); + verify(player).teleport(spawn, PlayerTeleportEvent.TeleportCause.PLUGIN); + } + @Test void mobReturnFallsBackToServerSpawnWithoutValidEntrance() { UUID ownerId = UUID.randomUUID();