feat(pocket-base): share flight through gold portals
Release / release (push) Successful in 2m50s
CI / build (push) Successful in 1m15s

This commit is contained in:
dmg
2026-08-29 00:20:40 -04:00
parent f4511f9286
commit e0b1a25c5e
21 changed files with 744 additions and 80 deletions
@@ -167,6 +167,122 @@ final class BaseFlightControllerTest {
verify(player).setAllowFlight(true);
}
@Test
void disabledAutomaticPocketBaseFlightDoesNotGrantOwnerFlight() {
UUID pocketWorldId = UUID.randomUUID();
World pocketWorld = mock(World.class);
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
PocketBaseState pocket = new PocketBaseState(playerId, 1, Optional.empty())
.withFlightUnlocked()
.withFlightEnabled(false);
BaseFlightController pocketController = new BaseFlightController(
server,
stateManager,
new SecondaryProgressionService(PluginSettings.from(Map.of())),
new BaseBoundsService(PluginSettings.from(Map.of())),
new PluginSettingsProvider(PluginSettings.from(Map.of())),
pocketBases,
Logger.getAnonymousLogger()
);
when(stateManager.player(playerId, "Alex")).thenReturn(
PlayerState.newPlayer(playerId, "Alex")
);
when(player.getWorld()).thenReturn(pocketWorld);
when(player.getAllowFlight()).thenReturn(false);
when(pocketWorld.getUID()).thenReturn(pocketWorldId);
when(pocketBases.ownerForPocketWorld(pocketWorldId)).thenReturn(Optional.of(playerId));
when(pocketBases.state(playerId)).thenReturn(pocket);
pocketController.run();
verify(player, never()).setAllowFlight(true);
}
@Test
void flightPortalTemporarilyGrantsGuestPocketBaseFlight() {
UUID ownerId = UUID.randomUUID();
UUID pocketWorldId = UUID.randomUUID();
World pocketWorld = mock(World.class);
Location location = mock(Location.class);
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
PocketBaseState pocket = new PocketBaseState(ownerId, 1, Optional.empty())
.withFlightUnlocked()
.withFlightEnabled(false);
BaseFlightController pocketController = new BaseFlightController(
server,
stateManager,
new SecondaryProgressionService(PluginSettings.from(Map.of())),
new BaseBoundsService(PluginSettings.from(Map.of())),
new PluginSettingsProvider(PluginSettings.from(Map.of())),
pocketBases,
Logger.getAnonymousLogger()
);
when(stateManager.player(playerId, "Alex")).thenReturn(
PlayerState.newPlayer(playerId, "Alex")
);
when(player.getWorld()).thenReturn(pocketWorld);
when(player.getLocation()).thenReturn(location);
when(player.getAllowFlight()).thenReturn(false);
when(pocketWorld.getUID()).thenReturn(pocketWorldId);
when(pocketBases.ownerForPocketWorld(pocketWorldId)).thenReturn(Optional.of(ownerId));
when(pocketBases.state(ownerId)).thenReturn(pocket);
when(pocketBases.policy()).thenReturn(
new PocketBasePolicy(PluginSettings.from(Map.of()))
);
pocketController.grantPocketPortalFlight(player, ownerId);
pocketController.run();
verify(player).setAllowFlight(true);
}
@Test
void portalFlightEndsAfterGuestLeavesPocketBase() {
UUID ownerId = UUID.randomUUID();
UUID pocketWorldId = UUID.randomUUID();
UUID normalWorldId = UUID.randomUUID();
World pocketWorld = mock(World.class);
World normalWorld = mock(World.class);
Location location = mock(Location.class);
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
PocketBaseState pocket = new PocketBaseState(ownerId, 1, Optional.empty())
.withFlightUnlocked()
.withFlightEnabled(false);
BaseFlightController pocketController = new BaseFlightController(
server,
stateManager,
new SecondaryProgressionService(PluginSettings.from(Map.of())),
new BaseBoundsService(PluginSettings.from(Map.of())),
new PluginSettingsProvider(PluginSettings.from(Map.of())),
pocketBases,
Logger.getAnonymousLogger()
);
when(stateManager.player(playerId, "Alex")).thenReturn(
PlayerState.newPlayer(playerId, "Alex")
);
when(player.getWorld()).thenReturn(pocketWorld, normalWorld);
when(player.getLocation()).thenReturn(location);
when(player.getAllowFlight()).thenReturn(false);
when(pocketWorld.getUID()).thenReturn(pocketWorldId);
when(normalWorld.getUID()).thenReturn(normalWorldId);
when(pocketBases.ownerForPocketWorld(pocketWorldId)).thenReturn(Optional.of(ownerId));
when(pocketBases.ownerForPocketWorld(normalWorldId)).thenReturn(Optional.empty());
when(pocketBases.state(ownerId)).thenReturn(pocket);
when(pocketBases.policy()).thenReturn(
new PocketBasePolicy(PluginSettings.from(Map.of()))
);
pocketController.grantPocketPortalFlight(player, ownerId);
pocketController.run();
pocketController.run();
verify(player).setFlying(false);
verify(player).setAllowFlight(false);
}
@Test
void flyingInPocketBaseBufferShowsLeavingWarning() {
UUID pocketWorldId = UUID.randomUUID();
@@ -15,6 +15,7 @@ import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
@@ -112,6 +113,16 @@ final class BaseSettingsCommandTest {
List.of("upgrade"),
command.onTabComplete(null, null, "basesettings", new String[] {"pocket", "u"})
);
assertEquals(
List.of("flight"),
command.onTabComplete(null, null, "basesettings", new String[] {"pocket", "f"})
);
assertEquals(
List.of("disable"),
command.onTabComplete(
null, null, "basesettings", new String[] {"pocket", "flight", "d"}
)
);
assertEquals(
List.of("mobs"),
command.onTabComplete(null, null, "basesettings", new String[] {"pocket", "m"})
@@ -459,6 +470,41 @@ final class BaseSettingsCommandTest {
assertEquals(64, restored[0].getAmount());
}
@Test
void ownerCanDisableAutomaticPocketBaseFlight() throws Exception {
UUID playerId = UUID.randomUUID();
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId);
when(player.getName()).thenReturn("Builder");
BaseStateManager stateManager = mock(BaseStateManager.class);
when(stateManager.player(playerId, "Builder")).thenReturn(
PlayerState.newPlayer(playerId, "Builder")
);
PocketBaseState enabled = new PocketBaseState(playerId, 1, Optional.empty())
.withFlightUnlocked();
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
when(pocketBases.state(playerId)).thenReturn(enabled);
when(pocketBases.setFlightEnabled(playerId, false)).thenReturn(
enabled.withFlightEnabled(false)
);
BaseSettingsCommand command = new BaseSettingsCommand(
stateManager,
new PluginSettingsProvider(PluginSettings.from(Map.of())),
mock(BaseFlightController.class),
pocketBases
);
command.onCommand(
player, null, "basesettings",
new String[] {"pocket", "flight", "disable"}
);
verify(pocketBases).setFlightEnabled(playerId, false);
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
message -> message.contains("automatic flight") && message.contains("disabled")
));
}
@Test
void statusDisplaysPocketMobSpawningPreferences() {
UUID playerId = UUID.randomUUID();
@@ -28,6 +28,15 @@ final class BaseSettingsDialogFactoryTest {
assertEquals("/basesettings status", payload.value());
}
@Test
void createsBackButtonThatReturnsToMainSettingsDashboard() {
BaseSettingsDialogFactory.ButtonSpec back =
BaseSettingsDialogFactory.backButtonSpec();
assertEquals("Back", back.label());
assertEquals("/basesettings ui", back.command());
}
@Test
void createsControlsForEveryBaseSettingAndConfirmedPurchase() {
UUID playerId = UUID.randomUUID();
@@ -47,7 +56,8 @@ final class BaseSettingsDialogFactoryTest {
Optional.empty(),
true,
false,
PocketBaseBiome.PLAINS
PocketBaseBiome.PLAINS,
true
);
BaseSettingsDialogFactory.DialogSpec specification =
@@ -64,7 +74,7 @@ final class BaseSettingsDialogFactoryTest {
root.dialogs().get(3)
);
assertTrue(pocketSettings.content().message().contains(
"Flight: Complete a raid to unlock"
"Flight: Enabled"
));
List<String> commands = commands(root);
assertTrue(commands.contains("/basesettings status"));
@@ -75,6 +85,7 @@ final class BaseSettingsDialogFactoryTest {
assertTrue(commands.contains("/basesettings spawnable disable"));
assertTrue(commands.contains("/basesettings bossbar disable"));
assertTrue(commands.contains("/basesettings pocket upgrade"));
assertTrue(commands.contains("/basesettings pocket flight disable"));
assertTrue(commands.contains("/basesettings pocket mobs hostile disable"));
assertTrue(commands.contains("/basesettings pocket mobs passive enable"));
assertTrue(commands.contains("/basesettings pocket type nether crimson_forest"));
@@ -24,6 +24,7 @@ import org.bukkit.entity.Entity;
import org.bukkit.entity.Mob;
import org.bukkit.entity.Player;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerMoveEvent;
@@ -150,6 +151,62 @@ final class PocketBaseControllerTest {
verify(event).setCancelled(true);
}
@Test
void ownerCanActivateGoldFlightPortalAfterRaidUnlock() throws Exception {
UUID ownerId = UUID.randomUUID();
UUID worldId = UUID.randomUUID();
PocketPortalLocation portal = new PocketPortalLocation(
worldId, "pocket", 8, 65, 8, PocketPortalAxis.X
);
Set<BlockPosition> frame = new HashSet<>(PocketPortalGeometry.frameBlocks(portal));
Player owner = mock(Player.class);
World world = mock(World.class);
Block clicked = block(world, 8, 67, 8, Material.GOLD_BLOCK);
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
ItemStack activator = item(Material.FLINT_AND_STEEL);
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
PluginSettings settings = PluginSettings.from(Map.of());
PocketBaseState state = new PocketBaseState(ownerId, 1, Optional.empty())
.withFlightUnlocked();
when(owner.getUniqueId()).thenReturn(ownerId);
when(event.getPlayer()).thenReturn(owner);
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
when(event.getClickedBlock()).thenReturn(clicked);
when(event.getItem()).thenReturn(activator);
when(world.getUID()).thenReturn(worldId);
when(world.getName()).thenReturn("pocket");
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
BlockPosition position = new BlockPosition(
invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)
);
return block(
world,
position.x(),
position.y(),
position.z(),
frame.contains(position) ? Material.GOLD_BLOCK : Material.AIR
);
});
when(pocketBases.ownerForPocketWorld(worldId)).thenReturn(Optional.of(ownerId));
when(pocketBases.state(ownerId)).thenReturn(state);
when(pocketBases.policy()).thenReturn(new PocketBasePolicy(settings));
PocketBaseController controller = new PocketBaseController(
mock(Server.class),
mock(BaseStateManager.class),
new BaseBoundsService(settings),
pocketBases,
new PluginSettingsProvider(settings),
Logger.getAnonymousLogger(),
mock(BaseFlightController.class)
);
controller.onActivate(event);
verify(pocketBases).activateFlightPortal(ownerId, portal);
verify(event).setCancelled(true);
}
@Test
void returnFrameOutsideUnlockedPocketBoundaryIsRejected() throws Exception {
UUID ownerId = UUID.randomUUID();
@@ -205,6 +262,39 @@ final class PocketBaseControllerTest {
verify(event).setCancelled(true);
}
@Test
void breakingActiveFlightPortalFrameDeactivatesIt() throws Exception {
UUID ownerId = UUID.randomUUID();
UUID worldId = UUID.randomUUID();
PocketPortalLocation flightPortal = new PocketPortalLocation(
worldId, "pocket", 8, 65, 8, PocketPortalAxis.X
);
PocketBaseState state = new PocketBaseState(ownerId, 1, Optional.empty())
.withFlightUnlocked()
.withFlightPortal(flightPortal);
World world = mock(World.class);
BlockBreakEvent event = mock(BlockBreakEvent.class);
Block broken = block(world, 8, 65, 8, Material.GOLD_BLOCK);
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
PluginSettings settings = PluginSettings.from(Map.of());
when(world.getUID()).thenReturn(worldId);
when(event.getBlock()).thenReturn(broken);
when(pocketBases.knownStates()).thenReturn(Map.of(ownerId, state));
PocketBaseController controller = new PocketBaseController(
mock(Server.class),
mock(BaseStateManager.class),
new BaseBoundsService(settings),
pocketBases,
new PluginSettingsProvider(settings),
Logger.getAnonymousLogger()
);
controller.onBreak(event);
verify(pocketBases).deactivateFlightPortal(ownerId);
}
@Test
void placementOutsideUnlockedPocketBoundaryIsCancelled() {
UUID ownerId = UUID.randomUUID();
@@ -311,6 +401,63 @@ final class PocketBaseControllerTest {
);
}
@Test
void activeFlightPortalGrantsTemporaryFlightToGuest() {
UUID ownerId = UUID.randomUUID();
UUID guestId = UUID.randomUUID();
UUID pocketWorldId = UUID.randomUUID();
PocketPortalLocation flightPortal = new PocketPortalLocation(
pocketWorldId, "pocket", 8, 65, 8, PocketPortalAxis.X
);
World pocketWorld = mock(World.class);
Set<BlockPosition> frame = new HashSet<>(
PocketPortalGeometry.frameBlocks(flightPortal)
);
Location destination = new Location(pocketWorld, 9.5, 66.0, 8.5);
Player guest = mock(Player.class);
PlayerMoveEvent event = mock(PlayerMoveEvent.class);
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
BaseFlightController flightController = mock(BaseFlightController.class);
Server server = mock(Server.class);
PluginSettings settings = PluginSettings.from(Map.of());
PocketBaseState state = new PocketBaseState(ownerId, 1, Optional.empty())
.withFlightUnlocked()
.withFlightPortal(flightPortal);
when(pocketWorld.getUID()).thenReturn(pocketWorldId);
when(pocketWorld.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
BlockPosition position = new BlockPosition(
invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)
);
return block(
pocketWorld,
position.x(),
position.y(),
position.z(),
frame.contains(position) ? Material.GOLD_BLOCK : Material.AIR
);
});
when(server.getWorld(pocketWorldId)).thenReturn(pocketWorld);
when(guest.getUniqueId()).thenReturn(guestId);
when(event.getPlayer()).thenReturn(guest);
when(event.getTo()).thenReturn(destination);
when(pocketBases.ownerForPocketWorld(pocketWorldId)).thenReturn(Optional.of(ownerId));
when(pocketBases.state(ownerId)).thenReturn(state);
PocketBaseController controller = new PocketBaseController(
server,
mock(BaseStateManager.class),
new BaseBoundsService(settings),
pocketBases,
new PluginSettingsProvider(settings),
Logger.getAnonymousLogger(),
flightController
);
controller.onMove(event);
verify(flightController).grantPocketPortalFlight(guest, ownerId);
}
@Test
void voidReturnClearsAccumulatedPlayerFallDistance() {
UUID ownerId = UUID.randomUUID();
@@ -32,7 +32,11 @@ final class YamlPocketBaseRepositoryTest {
true,
false,
PocketBaseBiome.CRIMSON_FOREST,
true
true,
Optional.of(new PocketPortalLocation(
worldId, "pocket", 4, 65, 4, PocketPortalAxis.X
)),
false
);
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(
temporaryDirectory.resolve("pocket-bases.yml")
@@ -59,6 +63,20 @@ final class YamlPocketBaseRepositoryTest {
assertFalse(loaded.flightUnlocked());
}
@Test
void existingRaidUnlockDefaultsOwnerFlightToEnabled() throws Exception {
UUID ownerId = UUID.randomUUID();
Path stateFile = temporaryDirectory.resolve("pocket-bases.yml");
Files.writeString(stateFile, "owners:\n " + ownerId
+ ":\n level: 1\n flight-unlocked: true\n");
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(stateFile);
PocketBaseState loaded = repository.load().get(ownerId);
assertTrue(loaded.flightEnabled());
assertTrue(loaded.flightPortal().isEmpty());
}
@Test
void migratesLegacyMobPreferenceToBothCategories() throws Exception {
UUID ownerId = UUID.randomUUID();