Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecb07d48ad | ||
|
|
c42ba6888a |
@@ -49,7 +49,7 @@ After 250 Survival-mode block placements anywhere by default, the spawnable over
|
||||
|
||||
`/base` has a stationary warm-up. Looking around is allowed, while movement between blocks, damage, teleportation, world changes, death, logout, and conflicting teleport commands cancel it without consuming the cooldown.
|
||||
|
||||
Base IV owners can purchase and expand a persistent Pocket Base with `/basesettings pocket upgrade`. Activating a complete diamond-block portal frame inside the normal base with flint and steel opens one public entrance to the owner's grass platform in a private void world. Players and non-player living mobs can travel through the entrance and active return portal; items, projectiles, and vehicles are not transported. The owner can move the return portal by building and igniting another complete frame inside the unlocked Pocket Base boundary; only the newly activated return portal remains functional. Natural hostile and passive mob spawning is disabled by default. Owners can control each category independently with `/basesettings pocket mobs <hostile|passive> <enable|disable>`. An owner who wins a raid inside their own Pocket Base permanently unlocks owner-only Survival flight across the full build height and within the current Pocket Base boundary plus a 16-block warning buffer on every side. The owner can control that automatic privilege with `/basesettings pocket flight <enable|disable>`. After the raid unlock, the owner can ignite a complete gold-block portal frame inside the unlocked boundary; any player who passes through receives temporary flight within the same boundary and buffer until leaving the Pocket Base. The owner's automatic-flight preference does not affect portal-granted flight.
|
||||
Base IV owners can purchase and expand a persistent Pocket Base with `/basesettings pocket upgrade`. Activating a complete diamond-block portal frame inside the normal base with flint and steel opens one public entrance to the owner's grass platform in a private void world. Players and non-player living mobs can travel through the entrance and active return portal; items, projectiles, and vehicles are not transported. The owner can move the return portal by building and igniting another complete frame inside the unlocked Pocket Base boundary; only the newly activated return portal remains functional. Natural hostile and passive mob spawning is disabled by default. Owners can control each category independently with `/basesettings pocket mobs <hostile|passive> <enable|disable>`. An owner who wins a raid inside their own Pocket Base permanently unlocks owner-only Survival flight across the full build height and within the current Pocket Base boundary plus a 16-block warning buffer on every side. The owner can control that automatic privilege with `/basesettings pocket flight <enable|disable>`. After the raid unlock, the owner can ignite a complete gold-block portal frame inside the unlocked boundary; any Survival player who passes through receives temporary flight within the same boundary and buffer until leaving the Pocket Base. Entering the flight portal immediately starts the player flying and displays “Pocket dimension flight enabled!” once, without repeating while they stand inside the portal. The owner's automatic-flight preference does not affect portal-granted flight.
|
||||
|
||||
Pocket Base owners can also craft a reusable **Pocket Base Keystone**. Its shaped recipe uses an Ender Pearl in the center, Netherite Ingots on the four cardinal slots, and Obsidian in the four corners. Right-clicking the keystone outside a Pocket Base instantly records the player's location and takes them to their own Pocket Base. Right-clicking it again inside their own Pocket Base returns them to that exact location when the world and destination remain available and safe. Saved returns survive reconnects and server restarts. A transferred keystone only works for a player who has unlocked their own Pocket Base.
|
||||
|
||||
|
||||
@@ -165,6 +165,11 @@ description: Chronological record of material decisions affecting the Spigot Bas
|
||||
- Restored Back buttons from nested native dialog screens to the main Base Settings dashboard.
|
||||
- Verified 149 tests and the Java 25 plugin artifact with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-29 — Recovery Compass metadata compatibility
|
||||
|
||||
- Accepted Purpur's valid generic item metadata when creating the Pocket Base Keystone instead of requiring the legacy Compass-specific metadata subtype.
|
||||
- Added regression coverage and verified 158 tests and the Java 25 plugin artifact with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-29 — Pocket Base Keystone travel
|
||||
|
||||
- Added a reusable Recovery Compass keystone recipe with an Ender Pearl center, cardinal Netherite Ingots, and Obsidian corners.
|
||||
|
||||
@@ -11,6 +11,10 @@ As a **Pocket Base owner**, I want an activated flight portal inside my Pocket B
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] Entering the active flight portal immediately starts eligible Survival players flying without double-jumping.
|
||||
- [x] Each portal entry sends “Pocket dimension flight enabled!” without repeated messages while standing in the portal.
|
||||
- [x] Immediate takeoff preserves existing flight boundaries and expiry rules, with regression tests.
|
||||
|
||||
- [x] The owner must first unlock Pocket Base flight by winning a raid in their own Pocket Base.
|
||||
- [x] Only the owner can activate a complete 4-by-5 gold-block frame with flint and steel as the active flight portal.
|
||||
- [x] Every required frame block must be inside the owner's currently unlocked Pocket Base boundary at activation.
|
||||
|
||||
@@ -12,6 +12,7 @@ As a **Pocket Base owner**, I want to craft a reusable keystone that instantly t
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] The Pocket Base Keystone is a distinct Recovery Compass identified by persistent item metadata rather than its display name alone.
|
||||
- [x] Keystone creation accepts the valid generic item metadata supplied for Recovery Compasses by the supported Purpur runtime.
|
||||
- [x] Its shaped recipe places an Ender Pearl in the center, Netherite Ingots on the four cardinal slots, and Obsidian in the four corners.
|
||||
- [x] Only a player who has unlocked Pocket Base I can complete the recipe or use a keystone.
|
||||
- [x] Right-clicking a keystone outside a Pocket Base instantly teleports the player to their own Pocket Base arrival point.
|
||||
|
||||
@@ -107,7 +107,18 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
}
|
||||
|
||||
void grantPocketPortalFlight(Player player, UUID ownerId) {
|
||||
if (player.getGameMode() != GameMode.SURVIVAL || pocketBases == null
|
||||
|| !pocketBases.ownerForPocketWorld(player.getWorld().getUID())
|
||||
.filter(ownerId::equals).isPresent()) {
|
||||
return;
|
||||
}
|
||||
portalFlight.put(player.getUniqueId(), ownerId);
|
||||
if (applyPocketFlight(player, ownerId)) {
|
||||
player.setFlying(true);
|
||||
player.sendMessage(ChatColor.GREEN + "Pocket dimension flight enabled!");
|
||||
} else {
|
||||
portalFlight.remove(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -220,14 +231,14 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPocketFlight(Player player, UUID ownerId) {
|
||||
private boolean applyPocketFlight(Player player, UUID ownerId) {
|
||||
PocketBaseState pocket = pocketBases.state(ownerId);
|
||||
boolean ownerFlight = ownerId.equals(player.getUniqueId())
|
||||
&& pocket.flightUnlocked() && pocket.flightEnabled();
|
||||
boolean temporaryFlight = ownerId.equals(portalFlight.get(player.getUniqueId()));
|
||||
if ((!ownerFlight && !temporaryFlight) || pocket.level() < 1) {
|
||||
removeGrantedFlight(player);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
int halfSize = pocketBases.policy().size(pocket.level()) / 2;
|
||||
int x = player.getLocation().getBlockX();
|
||||
@@ -237,7 +248,7 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
|| z < -halfSize - POCKET_FLIGHT_BUFFER
|
||||
|| z >= halfSize + POCKET_FLIGHT_BUFFER) {
|
||||
removeGrantedFlight(player);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (!player.getAllowFlight()) {
|
||||
player.setAllowFlight(true);
|
||||
@@ -257,6 +268,7 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
} else {
|
||||
warned.remove(player.getUniqueId());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void clearPortalFlight(Player player) {
|
||||
|
||||
@@ -246,7 +246,8 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
if (isIntact(portal, Material.GOLD_BLOCK)
|
||||
&& PocketPortalGeometry.isInterior(
|
||||
portal, position(destination)
|
||||
)) {
|
||||
) && (event.getFrom() == null
|
||||
|| !PocketPortalGeometry.isInterior(portal, position(event.getFrom())))) {
|
||||
flightController.grantPocketPortalFlight(player, ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.ShapedRecipe;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.inventory.meta.CompassMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
final class PocketBaseKeystoneService implements Listener {
|
||||
@@ -201,8 +200,13 @@ final class PocketBaseKeystoneService implements Listener {
|
||||
|
||||
static ItemStack createKeystone(NamespacedKey markerKey) {
|
||||
ItemStack item = new ItemStack(Material.RECOVERY_COMPASS);
|
||||
decorateKeystone(item, markerKey);
|
||||
return item;
|
||||
}
|
||||
|
||||
static void decorateKeystone(ItemStack item, NamespacedKey markerKey) {
|
||||
ItemMeta metadata = item.getItemMeta();
|
||||
if (!(metadata instanceof CompassMeta)) {
|
||||
if (metadata == null) {
|
||||
throw new IllegalStateException("Recovery Compass metadata is unavailable");
|
||||
}
|
||||
metadata.displayName(Component.text(
|
||||
@@ -218,7 +222,6 @@ final class PocketBaseKeystoneService implements Listener {
|
||||
markerKey, PersistentDataType.BYTE, (byte) 1
|
||||
);
|
||||
item.setItemMeta(metadata);
|
||||
return item;
|
||||
}
|
||||
|
||||
static RecipeSpec recipeSpecification() {
|
||||
|
||||
@@ -232,10 +232,28 @@ final class BaseFlightControllerTest {
|
||||
new PocketBasePolicy(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
when(player.getGameMode()).thenReturn(org.bukkit.GameMode.CREATIVE);
|
||||
pocketController.grantPocketPortalFlight(player, ownerId);
|
||||
verify(player, never()).setFlying(true);
|
||||
verify(player, never()).sendMessage(anyString());
|
||||
|
||||
when(player.getGameMode()).thenReturn(org.bukkit.GameMode.SURVIVAL);
|
||||
when(location.getBlockX()).thenReturn(10000);
|
||||
pocketController.grantPocketPortalFlight(player, ownerId);
|
||||
verify(player, never()).setFlying(true);
|
||||
verify(player, never()).sendMessage(anyString());
|
||||
|
||||
when(location.getBlockX()).thenReturn(0);
|
||||
pocketController.grantPocketPortalFlight(player, ownerId);
|
||||
pocketController.run();
|
||||
|
||||
verify(player).setAllowFlight(true);
|
||||
verify(player).setFlying(true);
|
||||
verify(player).sendMessage(org.bukkit.ChatColor.GREEN + "Pocket dimension flight enabled!");
|
||||
|
||||
when(player.getAllowFlight()).thenReturn(true);
|
||||
pocketController.grantPocketPortalFlight(player, ownerId);
|
||||
verify(player, times(2)).setFlying(true);
|
||||
verify(player, times(2)).sendMessage(org.bukkit.ChatColor.GREEN + "Pocket dimension flight enabled!");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -456,6 +456,15 @@ final class PocketBaseControllerTest {
|
||||
controller.onMove(event);
|
||||
|
||||
verify(flightController).grantPocketPortalFlight(guest, ownerId);
|
||||
|
||||
when(event.getFrom()).thenReturn(destination);
|
||||
controller.onMove(event);
|
||||
verify(flightController).grantPocketPortalFlight(guest, ownerId);
|
||||
|
||||
when(event.getFrom()).thenReturn(new Location(pocketWorld, 5, 66, 8));
|
||||
controller.onMove(event);
|
||||
verify(flightController, org.mockito.Mockito.times(2))
|
||||
.grantPocketPortalFlight(guest, ownerId);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -34,6 +34,22 @@ import org.bukkit.persistence.PersistentDataType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class PocketBaseKeystoneServiceTest {
|
||||
@Test
|
||||
void createsKeystoneWithGenericRecoveryCompassMetadata() {
|
||||
NamespacedKey key = NamespacedKey.minecraft("pocket_base_keystone");
|
||||
ItemStack item = mock(ItemStack.class);
|
||||
ItemMeta genericMetadata = mock(ItemMeta.class);
|
||||
PersistentDataContainer data = mock(PersistentDataContainer.class);
|
||||
|
||||
when(item.getItemMeta()).thenReturn(genericMetadata);
|
||||
when(genericMetadata.getPersistentDataContainer()).thenReturn(data);
|
||||
|
||||
PocketBaseKeystoneService.decorateKeystone(item, key);
|
||||
|
||||
verify(data).set(key, PersistentDataType.BYTE, (byte) 1);
|
||||
verify(item).setItemMeta(genericMetadata);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recipeUsesNetheriteOnCardinalsAndObsidianInCorners() {
|
||||
PocketBaseKeystoneService.RecipeSpec recipe =
|
||||
|
||||
Reference in New Issue
Block a user