feat(pocket-base): share flight through gold portals
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@@ -14,6 +16,8 @@ import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.raid.RaidFinishEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
@@ -28,6 +32,7 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
private final Logger logger;
|
||||
private final Set<UUID> grantedFlight = new HashSet<>();
|
||||
private final Set<UUID> warned = new HashSet<>();
|
||||
private final Map<UUID, UUID> portalFlight = new HashMap<>();
|
||||
|
||||
BaseFlightController(
|
||||
Server server,
|
||||
@@ -91,6 +96,20 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDeath(PlayerDeathEvent event) {
|
||||
clearPortalFlight(event.getEntity());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
clearPortalFlight(event.getPlayer());
|
||||
}
|
||||
|
||||
void grantPocketPortalFlight(Player player, UUID ownerId) {
|
||||
portalFlight.put(player.getUniqueId(), ownerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
for (Player player : server.getOnlinePlayers()) {
|
||||
@@ -102,6 +121,7 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
}
|
||||
grantedFlight.removeIf(id -> server.getPlayer(id) == null);
|
||||
warned.removeIf(id -> server.getPlayer(id) == null);
|
||||
portalFlight.keySet().removeIf(id -> server.getPlayer(id) == null);
|
||||
}
|
||||
|
||||
void removeGrantedFlight(Player player) {
|
||||
@@ -124,6 +144,7 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
}
|
||||
grantedFlight.clear();
|
||||
warned.clear();
|
||||
portalFlight.clear();
|
||||
}
|
||||
|
||||
private PlayerState observeElytra(Player player, PlayerState state) {
|
||||
@@ -148,6 +169,7 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
|
||||
private void applyFlight(Player player, PlayerState state) {
|
||||
if (player.getGameMode() != GameMode.SURVIVAL) {
|
||||
portalFlight.remove(player.getUniqueId());
|
||||
removeGrantedFlight(player);
|
||||
return;
|
||||
}
|
||||
@@ -160,6 +182,7 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
return;
|
||||
}
|
||||
}
|
||||
portalFlight.remove(player.getUniqueId());
|
||||
if (!state.flightEnabled() || state.flightLevel() < 1 || state.base().isEmpty()) {
|
||||
removeGrantedFlight(player);
|
||||
return;
|
||||
@@ -199,8 +222,10 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
|
||||
private void applyPocketFlight(Player player, UUID ownerId) {
|
||||
PocketBaseState pocket = pocketBases.state(ownerId);
|
||||
if (!ownerId.equals(player.getUniqueId()) || !pocket.flightUnlocked()
|
||||
|| pocket.level() < 1) {
|
||||
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;
|
||||
}
|
||||
@@ -234,6 +259,11 @@ final class BaseFlightController implements Runnable, Listener {
|
||||
}
|
||||
}
|
||||
|
||||
private void clearPortalFlight(Player player) {
|
||||
portalFlight.remove(player.getUniqueId());
|
||||
removeGrantedFlight(player);
|
||||
}
|
||||
|
||||
private boolean withinVerticalRange(Player player, PlayerState state, BaseLocation base) {
|
||||
int y = player.getLocation().getBlockY();
|
||||
if (state.flightLevel() == 3) {
|
||||
|
||||
@@ -93,6 +93,10 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
&& arguments[1].equalsIgnoreCase("upgrade")) {
|
||||
return purchasePocketUpgrade(player, state);
|
||||
}
|
||||
if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket")
|
||||
&& arguments[1].equalsIgnoreCase("flight")) {
|
||||
return updatePocketFlight(player, arguments[2]);
|
||||
}
|
||||
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket")
|
||||
&& arguments[1].equalsIgnoreCase("mobs")) {
|
||||
return updatePocketMobSpawning(player, arguments[2], arguments[3]);
|
||||
@@ -264,6 +268,35 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean updatePocketFlight(Player player, String mode) {
|
||||
if (pocketBases == null) {
|
||||
player.sendMessage(ChatColor.RED + "Pocket Bases are currently unavailable.");
|
||||
return true;
|
||||
}
|
||||
PocketBaseState current = pocketBases.state(player.getUniqueId());
|
||||
if (!current.flightUnlocked()) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "Win a raid in your Pocket Base before enabling automatic flight.");
|
||||
return true;
|
||||
}
|
||||
Boolean enabled = enabledMode(mode);
|
||||
if (enabled == null) {
|
||||
sendUsage(player);
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
current = pocketBases.setFlightEnabled(player.getUniqueId(), enabled);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "The Pocket Base flight setting could not be changed.");
|
||||
return true;
|
||||
}
|
||||
player.sendMessage(ChatColor.YELLOW + "Pocket Base automatic flight is now "
|
||||
+ (current.flightEnabled() ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled")
|
||||
+ ChatColor.YELLOW + ".");
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean updatePocketMobSpawning(
|
||||
Player player,
|
||||
String category,
|
||||
@@ -567,7 +600,9 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
+ "; passive spawning="
|
||||
+ (pocket.passiveMobSpawningEnabled() ? "enabled" : "disabled")
|
||||
+ "; flight="
|
||||
+ (pocket.flightUnlocked() ? "unlocked" : "complete a raid to unlock"));
|
||||
+ (pocket.flightUnlocked()
|
||||
? "automatic " + (pocket.flightEnabled() ? "enabled" : "disabled")
|
||||
: "complete a raid to unlock"));
|
||||
}
|
||||
|
||||
private void showCooldownPath(Player player, PlayerState state) {
|
||||
@@ -599,7 +634,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
List<String> modes = arguments[0].equalsIgnoreCase("visitors")
|
||||
? VISITOR_MODES
|
||||
: switch (arguments[0].toLowerCase(Locale.ROOT)) {
|
||||
case "pocket" -> List.of("upgrade", "mobs", "type");
|
||||
case "pocket" -> List.of("upgrade", "flight", "mobs", "type");
|
||||
case "navigation", "flight", "border", "spawnable", "bossbar" -> ENABLE_MODES;
|
||||
default -> List.of();
|
||||
};
|
||||
@@ -608,6 +643,11 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket")) {
|
||||
String prefix = arguments[2].toLowerCase(Locale.ROOT);
|
||||
if (arguments[1].equalsIgnoreCase("flight")) {
|
||||
return ENABLE_MODES.stream()
|
||||
.filter(mode -> mode.startsWith(prefix))
|
||||
.toList();
|
||||
}
|
||||
if (arguments[1].equalsIgnoreCase("mobs")) {
|
||||
return List.of("hostile", "passive").stream()
|
||||
.filter(category -> category.startsWith(prefix))
|
||||
@@ -701,7 +741,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private static void sendUsage(Player player) {
|
||||
player.sendMessage(ChatColor.RED + "Usage: /basesettings "
|
||||
+ "[ui|status|upgrade|pocket upgrade|pocket mobs "
|
||||
+ "[ui|status|upgrade|pocket upgrade|pocket flight <enable|disable>|pocket mobs "
|
||||
+ "<hostile|passive> <enable|disable>"
|
||||
+ "|pocket type <void|nether|overworld> <subtype>"
|
||||
+ "|visitors <allowed|blocked>|navigation <enable|disable>"
|
||||
|
||||
@@ -147,6 +147,7 @@ final class BaseSettingsDialogFactory {
|
||||
private DialogSpec pocketSettings(PlayerState owner, PocketBaseState pocket) {
|
||||
List<DialogSpec> dialogs = List.of(
|
||||
pocketUpgrade(owner, pocket),
|
||||
pocketFlightSettings(pocket),
|
||||
pocketMobSettings(pocket),
|
||||
pocketBiomeSettings(pocket)
|
||||
);
|
||||
@@ -161,7 +162,7 @@ final class BaseSettingsDialogFactory {
|
||||
+ enabled(pocket.hostileMobSpawningEnabled()) + " • Passive mobs: "
|
||||
+ enabled(pocket.passiveMobSpawningEnabled()) + "\nFlight: "
|
||||
+ (pocket.flightUnlocked()
|
||||
? "Unlocked"
|
||||
? (pocket.flightEnabled() ? "Enabled" : "Disabled")
|
||||
: "Complete a raid to unlock")
|
||||
),
|
||||
dialogs,
|
||||
@@ -191,6 +192,32 @@ final class BaseSettingsDialogFactory {
|
||||
);
|
||||
}
|
||||
|
||||
private DialogSpec pocketFlightSettings(PocketBaseState pocket) {
|
||||
if (!pocket.flightUnlocked()) {
|
||||
return notice(
|
||||
"Pocket Flight — Locked",
|
||||
"Win a raid in your Pocket Base to unlock automatic flight."
|
||||
);
|
||||
}
|
||||
return new MultiSpec(
|
||||
content(
|
||||
"Pocket Flight",
|
||||
"Pocket Flight",
|
||||
"Control automatic owner flight. Flight granted by the gold portal "
|
||||
+ "is unaffected."
|
||||
),
|
||||
List.of(toggle(
|
||||
"Automatic Flight",
|
||||
true,
|
||||
pocket.flightEnabled(),
|
||||
"Enabled",
|
||||
"Disabled",
|
||||
"basesettings pocket flight " + mode(!pocket.flightEnabled())
|
||||
)),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
private DialogSpec pocketMobSettings(PocketBaseState pocket) {
|
||||
if (pocket.level() < 1) {
|
||||
return notice(
|
||||
@@ -328,14 +355,19 @@ final class BaseSettingsDialogFactory {
|
||||
}
|
||||
|
||||
private static Dialog render(DialogSpec specification) {
|
||||
return render(specification, true);
|
||||
}
|
||||
|
||||
private static Dialog render(DialogSpec specification, boolean root) {
|
||||
DialogType type;
|
||||
ActionButton back = root ? null : backButton();
|
||||
if (specification instanceof ListSpec list) {
|
||||
List<Dialog> dialogs = list.dialogs().stream()
|
||||
.map(BaseSettingsDialogFactory::render)
|
||||
.map(child -> render(child, false))
|
||||
.toList();
|
||||
type = DialogType.dialogList(
|
||||
RegistrySet.valueSet(RegistryKey.DIALOG, dialogs),
|
||||
null,
|
||||
back,
|
||||
list.columns(),
|
||||
list.buttonWidth()
|
||||
);
|
||||
@@ -343,13 +375,15 @@ final class BaseSettingsDialogFactory {
|
||||
List<ActionButton> actions = multi.actions().stream()
|
||||
.map(BaseSettingsDialogFactory::render)
|
||||
.toList();
|
||||
type = DialogType.multiAction(actions, null, multi.columns());
|
||||
type = DialogType.multiAction(actions, back, multi.columns());
|
||||
} else if (specification instanceof ConfirmationSpec confirmation) {
|
||||
ActionButton cancel = ActionButton.builder(Component.text("Cancel")).build();
|
||||
type = DialogType.confirmation(render(confirmation.confirm()), cancel);
|
||||
type = DialogType.confirmation(
|
||||
render(confirmation.confirm()),
|
||||
root ? ActionButton.builder(Component.text("Cancel")).build() : back
|
||||
);
|
||||
} else if (specification instanceof NoticeSpec notice) {
|
||||
type = notice.action() == null
|
||||
? DialogType.notice()
|
||||
? (root ? DialogType.notice() : DialogType.notice(back))
|
||||
: DialogType.notice(render(notice.action()));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported dialog specification");
|
||||
@@ -367,6 +401,14 @@ final class BaseSettingsDialogFactory {
|
||||
return Dialog.create(factory -> factory.empty().base(base).type(type));
|
||||
}
|
||||
|
||||
private static ActionButton backButton() {
|
||||
return render(backButtonSpec());
|
||||
}
|
||||
|
||||
static ButtonSpec backButtonSpec() {
|
||||
return commandButton("Back", "basesettings ui");
|
||||
}
|
||||
|
||||
private static ActionButton render(ButtonSpec button) {
|
||||
ActionButton.Builder builder = ActionButton.builder(Component.text(button.label()))
|
||||
.width(button.width())
|
||||
|
||||
@@ -39,6 +39,7 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
private final PocketBaseManager pocketBases;
|
||||
private final PluginSettingsProvider settings;
|
||||
private final Logger logger;
|
||||
private final BaseFlightController flightController;
|
||||
private final Map<UUID, Long> cooldownUntil = new HashMap<>();
|
||||
|
||||
PocketBaseController(
|
||||
@@ -48,6 +49,18 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
PocketBaseManager pocketBases,
|
||||
PluginSettingsProvider settings,
|
||||
Logger logger
|
||||
) {
|
||||
this(server, baseStates, baseBounds, pocketBases, settings, logger, null);
|
||||
}
|
||||
|
||||
PocketBaseController(
|
||||
Server server,
|
||||
BaseStateManager baseStates,
|
||||
BaseBoundsService baseBounds,
|
||||
PocketBaseManager pocketBases,
|
||||
PluginSettingsProvider settings,
|
||||
Logger logger,
|
||||
BaseFlightController flightController
|
||||
) {
|
||||
this.server = server;
|
||||
this.baseStates = baseStates;
|
||||
@@ -55,6 +68,7 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
this.pocketBases = pocketBases;
|
||||
this.settings = settings;
|
||||
this.logger = logger;
|
||||
this.flightController = flightController;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -65,6 +79,8 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
if (state.level() > 0 && pocketBases.returnPortalIsIntact(state.ownerId())) {
|
||||
showPortal(pocketBases.returnPortal(state.ownerId()));
|
||||
}
|
||||
state.flightPortal().filter(portal -> isIntact(portal, Material.GOLD_BLOCK))
|
||||
.ifPresent(portal -> showPortal(portal, Material.GOLD_BLOCK));
|
||||
}
|
||||
scanMobPortals();
|
||||
long now = System.nanoTime();
|
||||
@@ -88,41 +104,57 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
Material frameMaterial = Material.valueOf(
|
||||
settings.current().pocketBasePortalFrameMaterial()
|
||||
);
|
||||
Optional<PocketPortalLocation> portal = PocketPortalGeometry.findFrame(
|
||||
position(clicked),
|
||||
world.getUID(),
|
||||
world.getName(),
|
||||
candidate -> block(world, candidate).getType() == frameMaterial,
|
||||
candidate -> isAir(block(world, candidate).getType())
|
||||
);
|
||||
if (portal.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Optional<UUID> pocketOwner = pocketBases.ownerForPocketWorld(world.getUID());
|
||||
if (pocketOwner.isPresent()) {
|
||||
if (!pocketOwner.orElseThrow().equals(player.getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
Optional<PocketPortalLocation> flightPortal = pocket.flightUnlocked()
|
||||
? findFrame(clicked, Material.GOLD_BLOCK)
|
||||
: Optional.empty();
|
||||
boolean activatingFlightPortal = flightPortal.isPresent();
|
||||
Optional<PocketPortalLocation> portal = activatingFlightPortal
|
||||
? flightPortal
|
||||
: findFrame(clicked, frameMaterial);
|
||||
if (portal.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (PocketPortalGeometry.frameBlocks(portal.orElseThrow()).stream()
|
||||
.anyMatch(candidate -> !pocketBases.policy().contains(
|
||||
pocket.level(), candidate.x(), candidate.z()
|
||||
))) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "The complete return portal must be inside your Pocket Base boundary.");
|
||||
player.sendMessage(ChatColor.RED + "The complete "
|
||||
+ (activatingFlightPortal ? "flight" : "return")
|
||||
+ " portal must be inside your Pocket Base boundary.");
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
pocketBases.activateReturnPortal(player.getUniqueId(), portal.orElseThrow());
|
||||
if (activatingFlightPortal) {
|
||||
pocketBases.activateFlightPortal(
|
||||
player.getUniqueId(), portal.orElseThrow()
|
||||
);
|
||||
} else {
|
||||
pocketBases.activateReturnPortal(
|
||||
player.getUniqueId(), portal.orElseThrow()
|
||||
);
|
||||
}
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
logger.log(Level.SEVERE, "Could not activate Pocket Base return portal", exception);
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "The Pocket Base return portal could not be activated.");
|
||||
logger.log(Level.SEVERE, "Could not activate Pocket Base "
|
||||
+ (activatingFlightPortal ? "flight" : "return") + " portal", exception);
|
||||
player.sendMessage(ChatColor.RED + "The Pocket Base "
|
||||
+ (activatingFlightPortal ? "flight" : "return")
|
||||
+ " portal could not be activated.");
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
event.setCancelled(true);
|
||||
player.sendMessage(ChatColor.GREEN + "Pocket Base return portal activated.");
|
||||
player.sendMessage(ChatColor.GREEN + "Pocket Base "
|
||||
+ (activatingFlightPortal ? "flight" : "return") + " portal activated.");
|
||||
return;
|
||||
}
|
||||
Optional<PocketPortalLocation> portal = findFrame(clicked, frameMaterial);
|
||||
if (portal.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
PlayerState owner = baseStates.player(player.getUniqueId(), player.getName());
|
||||
@@ -161,6 +193,13 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
deactivate(state.ownerId());
|
||||
}
|
||||
}
|
||||
if (state.flightPortal().isPresent()) {
|
||||
PocketPortalLocation portal = state.flightPortal().orElseThrow();
|
||||
if (portal.worldId().equals(worldId)
|
||||
&& PocketPortalGeometry.isFrame(portal, broken)) {
|
||||
deactivateFlightPortal(state.ownerId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +238,17 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
pocketBases.returnPortal(ownerId), position(destination)
|
||||
)) {
|
||||
leavePocket(player, ownerId);
|
||||
return;
|
||||
}
|
||||
PocketBaseState state = pocketBases.state(ownerId);
|
||||
if (flightController != null && state.flightPortal().isPresent()) {
|
||||
PocketPortalLocation portal = state.flightPortal().orElseThrow();
|
||||
if (isIntact(portal, Material.GOLD_BLOCK)
|
||||
&& PocketPortalGeometry.isInterior(
|
||||
portal, position(destination)
|
||||
)) {
|
||||
flightController.grantPocketPortalFlight(player, ownerId);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -316,18 +366,26 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
}
|
||||
|
||||
private boolean isIntact(PocketPortalLocation portal) {
|
||||
World world = server.getWorld(portal.worldId());
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
Material frame = Material.valueOf(settings.current().pocketBasePortalFrameMaterial());
|
||||
return PocketPortalGeometry.frameBlocks(portal).stream()
|
||||
return isIntact(portal, frame);
|
||||
}
|
||||
|
||||
private boolean isIntact(PocketPortalLocation portal, Material frame) {
|
||||
World world = server.getWorld(portal.worldId());
|
||||
return world != null && PocketPortalGeometry.frameBlocks(portal).stream()
|
||||
.allMatch(position -> block(world, position).getType() == frame);
|
||||
}
|
||||
|
||||
private void showPortal(PocketPortalLocation portal) {
|
||||
showPortal(
|
||||
portal,
|
||||
Material.valueOf(settings.current().pocketBasePortalFrameMaterial())
|
||||
);
|
||||
}
|
||||
|
||||
private void showPortal(PocketPortalLocation portal, Material frame) {
|
||||
World world = server.getWorld(portal.worldId());
|
||||
if (world == null || !isIntact(portal)) {
|
||||
if (world == null || !isIntact(portal, frame)) {
|
||||
return;
|
||||
}
|
||||
for (BlockPosition position : PocketPortalGeometry.interiorBlocks(portal)) {
|
||||
@@ -345,6 +403,14 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private void deactivateFlightPortal(UUID ownerId) {
|
||||
try {
|
||||
pocketBases.deactivateFlightPortal(ownerId);
|
||||
} catch (IOException exception) {
|
||||
logger.log(Level.SEVERE, "Could not deactivate Pocket Base flight portal", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void deactivate(UUID ownerId) {
|
||||
try {
|
||||
pocketBases.deactivateEntrance(ownerId);
|
||||
@@ -383,6 +449,17 @@ final class PocketBaseController implements Listener, Runnable {
|
||||
);
|
||||
}
|
||||
|
||||
private Optional<PocketPortalLocation> findFrame(Block clicked, Material material) {
|
||||
World world = clicked.getWorld();
|
||||
return PocketPortalGeometry.findFrame(
|
||||
position(clicked),
|
||||
world.getUID(),
|
||||
world.getName(),
|
||||
candidate -> block(world, candidate).getType() == material,
|
||||
candidate -> isAir(block(world, candidate).getType())
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean isAir(Material material) {
|
||||
return material == Material.AIR || material == Material.CAVE_AIR
|
||||
|| material == Material.VOID_AIR;
|
||||
|
||||
@@ -60,6 +60,24 @@ final class PocketBaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
PocketBaseState activateFlightPortal(
|
||||
UUID ownerId,
|
||||
PocketPortalLocation portal
|
||||
) throws IOException {
|
||||
PocketBaseState current = states.state(ownerId);
|
||||
if (!current.flightUnlocked()) {
|
||||
throw new IllegalStateException("Pocket Base flight is still locked");
|
||||
}
|
||||
return states.updateAndSave(ownerId, state -> state.withFlightPortal(portal));
|
||||
}
|
||||
|
||||
void deactivateFlightPortal(UUID ownerId) throws IOException {
|
||||
PocketBaseState current = states.state(ownerId);
|
||||
if (current.flightPortal().isPresent()) {
|
||||
states.updateAndSave(ownerId, PocketBaseState::withoutFlightPortal);
|
||||
}
|
||||
}
|
||||
|
||||
PocketBaseState activateReturnPortal(
|
||||
UUID ownerId,
|
||||
PocketPortalLocation portal
|
||||
@@ -103,6 +121,19 @@ final class PocketBaseManager {
|
||||
return states.updateAndSave(ownerId, PocketBaseState::withFlightUnlocked);
|
||||
}
|
||||
|
||||
PocketBaseState setFlightEnabled(UUID ownerId, boolean enabled) throws IOException {
|
||||
PocketBaseState current = states.state(ownerId);
|
||||
if (!current.flightUnlocked()) {
|
||||
throw new IllegalStateException("Pocket Base flight is still locked");
|
||||
}
|
||||
if (current.flightEnabled() == enabled) {
|
||||
return current;
|
||||
}
|
||||
return states.updateAndSave(
|
||||
ownerId, state -> state.withFlightEnabled(enabled)
|
||||
);
|
||||
}
|
||||
|
||||
PocketBaseState setHostileMobSpawning(UUID ownerId, boolean enabled)
|
||||
throws IOException {
|
||||
PocketBaseState current = states.state(ownerId);
|
||||
|
||||
@@ -11,7 +11,9 @@ public record PocketBaseState(
|
||||
boolean hostileMobSpawningEnabled,
|
||||
boolean passiveMobSpawningEnabled,
|
||||
PocketBaseBiome biome,
|
||||
boolean flightUnlocked
|
||||
boolean flightUnlocked,
|
||||
Optional<PocketPortalLocation> flightPortal,
|
||||
boolean flightEnabled
|
||||
) {
|
||||
public PocketBaseState {
|
||||
if (ownerId == null) {
|
||||
@@ -22,10 +24,34 @@ public record PocketBaseState(
|
||||
}
|
||||
entrance = entrance == null ? Optional.empty() : entrance;
|
||||
returnPortal = returnPortal == null ? Optional.empty() : returnPortal;
|
||||
flightPortal = flightPortal == null ? Optional.empty() : flightPortal;
|
||||
biome = biome == null ? PocketBaseBiome.THE_VOID : biome;
|
||||
if (level == 0 && (entrance.isPresent() || returnPortal.isPresent())) {
|
||||
throw new IllegalArgumentException("a locked Pocket Base cannot have a portal");
|
||||
if (level == 0 && (entrance.isPresent() || returnPortal.isPresent()
|
||||
|| flightPortal.isPresent() || flightUnlocked || flightEnabled)) {
|
||||
throw new IllegalArgumentException("a locked Pocket Base cannot have unlocks or portals");
|
||||
}
|
||||
if (!flightUnlocked && (flightPortal.isPresent() || flightEnabled)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Pocket Base flight must be unlocked before it is enabled or shared"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public PocketBaseState(
|
||||
UUID ownerId,
|
||||
int level,
|
||||
Optional<PocketPortalLocation> entrance,
|
||||
Optional<PocketPortalLocation> returnPortal,
|
||||
boolean hostileMobSpawningEnabled,
|
||||
boolean passiveMobSpawningEnabled,
|
||||
PocketBaseBiome biome,
|
||||
boolean flightUnlocked
|
||||
) {
|
||||
this(
|
||||
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked, Optional.empty(),
|
||||
flightUnlocked
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState(
|
||||
@@ -39,7 +65,7 @@ public record PocketBaseState(
|
||||
) {
|
||||
this(
|
||||
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, false
|
||||
passiveMobSpawningEnabled, biome, false, Optional.empty(), false
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,70 +76,88 @@ public record PocketBaseState(
|
||||
) {
|
||||
this(
|
||||
ownerId, level, entrance, Optional.empty(), false, false,
|
||||
PocketBaseBiome.THE_VOID, false
|
||||
PocketBaseBiome.THE_VOID, false, Optional.empty(), false
|
||||
);
|
||||
}
|
||||
|
||||
public static PocketBaseState locked(UUID ownerId) {
|
||||
return new PocketBaseState(
|
||||
ownerId, 0, Optional.empty(), Optional.empty(), false, false,
|
||||
PocketBaseBiome.THE_VOID, false
|
||||
PocketBaseBiome.THE_VOID, false, Optional.empty(), false
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState withLevel(int newLevel) {
|
||||
return new PocketBaseState(
|
||||
ownerId, newLevel, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked
|
||||
);
|
||||
return copy(newLevel, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked, flightPortal, flightEnabled);
|
||||
}
|
||||
|
||||
public PocketBaseState withEntrance(PocketPortalLocation portal) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, Optional.of(portal), returnPortal,
|
||||
hostileMobSpawningEnabled, passiveMobSpawningEnabled, biome, flightUnlocked
|
||||
);
|
||||
return copy(level, Optional.of(portal), returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked, flightPortal, flightEnabled);
|
||||
}
|
||||
|
||||
public PocketBaseState withoutEntrance() {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, Optional.empty(), returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked
|
||||
);
|
||||
return copy(level, Optional.empty(), returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked, flightPortal, flightEnabled);
|
||||
}
|
||||
|
||||
public PocketBaseState withReturnPortal(PocketPortalLocation portal) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, entrance, Optional.of(portal), hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked
|
||||
);
|
||||
return copy(level, entrance, Optional.of(portal), hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked, flightPortal, flightEnabled);
|
||||
}
|
||||
|
||||
public PocketBaseState withHostileMobSpawningEnabled(boolean enabled) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, entrance, returnPortal, enabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked
|
||||
);
|
||||
return copy(level, entrance, returnPortal, enabled, passiveMobSpawningEnabled,
|
||||
biome, flightUnlocked, flightPortal, flightEnabled);
|
||||
}
|
||||
|
||||
public PocketBaseState withPassiveMobSpawningEnabled(boolean enabled) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
enabled, biome, flightUnlocked
|
||||
);
|
||||
return copy(level, entrance, returnPortal, hostileMobSpawningEnabled, enabled,
|
||||
biome, flightUnlocked, flightPortal, flightEnabled);
|
||||
}
|
||||
|
||||
public PocketBaseState withBiome(PocketBaseBiome selectedBiome) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, selectedBiome, flightUnlocked
|
||||
);
|
||||
return copy(level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, selectedBiome, flightUnlocked, flightPortal,
|
||||
flightEnabled);
|
||||
}
|
||||
|
||||
public PocketBaseState withFlightUnlocked() {
|
||||
return copy(level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, true, flightPortal, true);
|
||||
}
|
||||
|
||||
public PocketBaseState withFlightEnabled(boolean enabled) {
|
||||
return copy(level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked, flightPortal, enabled);
|
||||
}
|
||||
|
||||
public PocketBaseState withFlightPortal(PocketPortalLocation portal) {
|
||||
return copy(level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked, Optional.of(portal),
|
||||
flightEnabled);
|
||||
}
|
||||
|
||||
public PocketBaseState withoutFlightPortal() {
|
||||
return copy(level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked, Optional.empty(), flightEnabled);
|
||||
}
|
||||
|
||||
private PocketBaseState copy(
|
||||
int newLevel,
|
||||
Optional<PocketPortalLocation> newEntrance,
|
||||
Optional<PocketPortalLocation> newReturnPortal,
|
||||
boolean hostileEnabled,
|
||||
boolean passiveEnabled,
|
||||
PocketBaseBiome selectedBiome,
|
||||
boolean unlockedFlight,
|
||||
Optional<PocketPortalLocation> newFlightPortal,
|
||||
boolean enabledFlight
|
||||
) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, true
|
||||
ownerId, newLevel, newEntrance, newReturnPortal, hostileEnabled, passiveEnabled,
|
||||
selectedBiome, unlockedFlight, newFlightPortal, enabledFlight
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,8 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
boundsService,
|
||||
pocketBaseManager,
|
||||
settingsProvider,
|
||||
getLogger()
|
||||
getLogger(),
|
||||
flightController
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(progressListener, this);
|
||||
getServer().getPluginManager().registerEvents(flightController, this);
|
||||
|
||||
@@ -39,6 +39,9 @@ public final class YamlPocketBaseRepository {
|
||||
UUID ownerId = UUID.fromString(key);
|
||||
String path = "owners." + key;
|
||||
int level = yaml.getInt(path + ".level", 0);
|
||||
boolean flightUnlocked = yaml.getBoolean(
|
||||
path + ".flight-unlocked", false
|
||||
);
|
||||
PocketBaseState state = new PocketBaseState(
|
||||
ownerId,
|
||||
level,
|
||||
@@ -47,7 +50,9 @@ public final class YamlPocketBaseRepository {
|
||||
loadMobPreference(yaml, path, "hostile-mob-spawning-enabled"),
|
||||
loadMobPreference(yaml, path, "passive-mob-spawning-enabled"),
|
||||
loadBiome(yaml, path),
|
||||
yaml.getBoolean(path + ".flight-unlocked", false)
|
||||
flightUnlocked,
|
||||
loadPortal(yaml, path + ".flight-portal"),
|
||||
yaml.getBoolean(path + ".flight-enabled", flightUnlocked)
|
||||
);
|
||||
states.put(ownerId, state);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
@@ -75,12 +80,16 @@ public final class YamlPocketBaseRepository {
|
||||
state.passiveMobSpawningEnabled()
|
||||
);
|
||||
yaml.set(path + ".flight-unlocked", state.flightUnlocked());
|
||||
yaml.set(path + ".flight-enabled", state.flightEnabled());
|
||||
yaml.set(path + ".world-type", state.biome().worldType().commandName());
|
||||
yaml.set(path + ".biome", state.biome().commandName());
|
||||
state.entrance().ifPresent(portal -> savePortal(yaml, path + ".entrance", portal));
|
||||
state.returnPortal().ifPresent(portal ->
|
||||
savePortal(yaml, path + ".return-portal", portal)
|
||||
);
|
||||
state.flightPortal().ifPresent(portal ->
|
||||
savePortal(yaml, path + ".flight-portal", portal)
|
||||
);
|
||||
}
|
||||
Path temporary = Files.createTempFile(parent, "spigot-base-pocket-", ".yml");
|
||||
try {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user