feat(pocket-base): unlock flight by completing raids
This commit is contained in:
@@ -48,7 +48,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>`.
|
||||
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.
|
||||
|
||||
Owners can change biome metadata without altering Pocket Base blocks, entities, inventories, or portals by using `/basesettings pocket type <void|nether|overworld> <subtype>`. Void uses `the_void`; Nether and Overworld accept their compatible vanilla biome names, such as `crimson_forest` or `plains`. Each change defaults to 16 netherite blocks, is configurable, and affects applicable natural mob selection when spawning is enabled. The Pocket Base remains a void-generated normal-environment world; dimension-specific mechanics do not change.
|
||||
|
||||
|
||||
@@ -149,3 +149,10 @@ description: Chronological record of material decisions affecting the Spigot Bas
|
||||
- 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`.
|
||||
|
||||
## 2026-08-28 — Raid-earned Pocket Base flight
|
||||
|
||||
- Made a successful raid victory by the owner inside their own Pocket Base permanently unlock owner-only Survival flight.
|
||||
- Limited Pocket Base flight to its expanding horizontal boundary plus a 16-block warning buffer while allowing the world's full build height.
|
||||
- Persisted unlocks with safe locked defaults for existing records and exposed progress through chat status and the native settings dialog.
|
||||
- Verified 140 tests and the Java 25 plugin artifact with `./gradlew clean check jar`.
|
||||
|
||||
@@ -26,3 +26,4 @@ description: Catalog of user stories for the Spigot Base plugin.
|
||||
18. [US-018: Control Pocket Base mob spawning](us-018-control-pocket-base-mob-spawning.md)
|
||||
19. [US-019: Change a Pocket Base biome](us-019-change-a-pocket-base-biome.md)
|
||||
20. [US-020: Control a base through a dialog UI](us-020-control-base-through-dialog-ui.md)
|
||||
21. [US-021: Unlock Pocket Base flight](us-021-unlock-pocket-base-flight.md)
|
||||
|
||||
@@ -12,6 +12,7 @@ As a **player**, I want to inspect my progression and receive timely milestone f
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `/basesettings status` shows the player's Base, Base Size, Base Flight, Teleport Warm-up, and Teleport Cooldown paths plus current visitor, navigation, flight, and boss-bar settings.
|
||||
- [x] `/basesettings status` shows whether Pocket Base flight still requires a successful owner raid or has been unlocked.
|
||||
- [x] Each path identifies earned levels, unmet prerequisites, current progress, the next threshold, and the next reward.
|
||||
- [x] Locked secondary paths clearly identify Base I or another sequential level as their prerequisite.
|
||||
- [x] Relevant qualifying activity briefly displays a configurable progress boss bar for the active milestone.
|
||||
@@ -31,3 +32,4 @@ As a **player**, I want to inspect my progression and receive timely milestone f
|
||||
- [US-001: Unlock and establish Base I](us-001-unlock-and-establish-base.md)
|
||||
- [US-004: Unlock and control base flight](us-004-unlock-and-control-base-flight.md)
|
||||
- [US-010: Administer player progression](us-010-administer-player-progression.md)
|
||||
- [US-021: Unlock Pocket Base flight](us-021-unlock-pocket-base-flight.md)
|
||||
|
||||
@@ -17,6 +17,7 @@ As a **server operator**, I want progression behavior to be configurable and dur
|
||||
- [x] Radii, vertical ranges, durations, and prices reject unsafe or nonsensical values.
|
||||
- [x] Invalid required configuration prevents partial plugin initialization and produces a clear server log message.
|
||||
- [x] UUID-keyed state persists latest known names, base locations, counters, levels, relocation times, teleport times, visitor cooldowns, navigation preferences, flight preferences, boss-bar preferences, and visitor settings.
|
||||
- [x] UUID-keyed Pocket Base state persists raid-earned flight unlocks, with missing legacy values defaulting safely to locked.
|
||||
- [x] Cooldowns and relocation limits use real elapsed timestamps and continue while players are offline.
|
||||
- [x] State is saved safely so a failed write does not replace valid persisted state with a partial document.
|
||||
- [x] Corrupt, unknown, or invalid records are handled defensively and cannot silently grant progression or privileges.
|
||||
@@ -28,3 +29,4 @@ As a **server operator**, I want progression behavior to be configurable and dur
|
||||
- [US-003: Expand the base](us-003-expand-the-base.md)
|
||||
- [US-010: Administer player progression](us-010-administer-player-progression.md)
|
||||
- [US-012: Build and release the plugin](us-012-build-and-release-plugin.md)
|
||||
- [US-021: Unlock Pocket Base flight](us-021-unlock-pocket-base-flight.md)
|
||||
|
||||
@@ -16,6 +16,7 @@ As a **player**, I want a dialog-based base dashboard so that I can inspect prog
|
||||
- [x] `/basesettings status` retains the chat-based progression report.
|
||||
- [x] The dialog summarizes current Base and Pocket Base progression.
|
||||
- [x] Dialog controls cover visitors, navigation, flight, border, spawnable overlay, boss bar, Pocket Base mob spawning, upgrades, and biome selection when applicable.
|
||||
- [x] The Pocket Base dialog shows whether owner flight still requires a successful raid or has been unlocked.
|
||||
- [x] Controls indicate their current or locked state.
|
||||
- [x] Purchases require confirmation and continue using existing validation, payment, and persistence logic.
|
||||
- [x] `ui` is offered through command autocomplete.
|
||||
@@ -32,3 +33,4 @@ As a **player**, I want a dialog-based base dashboard so that I can inspect prog
|
||||
- [US-015: Unlock a Pocket Base](us-015-unlock-a-pocket-base.md)
|
||||
- [US-018: Control Pocket Base mob spawning](us-018-control-pocket-base-mob-spawning.md)
|
||||
- [US-019: Change a Pocket Base biome](us-019-change-a-pocket-base-biome.md)
|
||||
- [US-021: Unlock Pocket Base flight](us-021-unlock-pocket-base-flight.md)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
type: User Story
|
||||
title: "US-021: Unlock Pocket Base flight"
|
||||
description: Let a Pocket Base owner permanently earn controlled flight by winning a raid in their own pocket world.
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-021: Unlock Pocket Base flight
|
||||
|
||||
As a **Pocket Base owner**, I want to earn flight by completing a raid in my Pocket Base so that I can build and travel safely throughout my expanding pocket world.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] Pocket Base flight begins locked for new owners and existing saved Pocket Bases.
|
||||
- [x] A successful raid completed in a Pocket Base permanently unlocks flight only when that Pocket Base's owner is listed among the raid winners.
|
||||
- [x] Raids completed outside the owner's Pocket Base and raids the owner does not win do not unlock flight.
|
||||
- [x] Defeated, stopped, expired, and abandoned raids do not unlock flight.
|
||||
- [x] Unlocking Pocket Base flight is free and does not consume blocks or other currency.
|
||||
- [x] The owner receives a prominent one-time unlock notification after the qualifying raid.
|
||||
- [x] In Survival mode, an owner with the unlock receives plugin-granted flight while inside their own Pocket Base.
|
||||
- [x] Pocket Base flight spans the world's full build height and the current unlocked horizontal boundary plus 16 blocks on every side.
|
||||
- [x] An actively flying owner receives a warning after leaving the unlocked Pocket Base boundary while remaining inside the 16-block buffer.
|
||||
- [x] Moving beyond the 16-block buffer removes only flight granted by this plugin.
|
||||
- [x] Pocket Base expansion automatically expands the corresponding flight area.
|
||||
- [x] Visitors do not receive the owner's Pocket Base flight privilege.
|
||||
- [x] Teleportation, world changes, game-mode changes, death, logout, and plugin shutdown do not leave unintended flight enabled.
|
||||
- [x] The unlock persists across reconnects and restarts.
|
||||
- [x] Chat status and the native settings dialog show whether Pocket Base flight is locked behind a successful raid or unlocked.
|
||||
|
||||
## Related
|
||||
|
||||
- [US-004: Unlock and control base flight](us-004-unlock-and-control-base-flight.md)
|
||||
- [US-009: View progression and unlock notifications](us-009-view-progression-and-notifications.md)
|
||||
- [US-011: Configure and persist progression](us-011-configure-and-persist-progression.md)
|
||||
- [US-015: Unlock a Pocket Base](us-015-unlock-a-pocket-base.md)
|
||||
- [US-017: Expand a Pocket Base](us-017-expand-a-pocket-base.md)
|
||||
- [US-020: Control a base through a dialog UI](us-020-control-base-through-dialog-ui.md)
|
||||
@@ -1,21 +1,31 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.raid.RaidFinishEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
final class BaseFlightController implements Runnable {
|
||||
final class BaseFlightController implements Runnable, Listener {
|
||||
private static final int POCKET_FLIGHT_BUFFER = 16;
|
||||
private final Server server;
|
||||
private final BaseStateManager stateManager;
|
||||
private final SecondaryProgressionService progressionService;
|
||||
private final BaseBoundsService boundsService;
|
||||
private final PluginSettingsProvider settings;
|
||||
private final PocketBaseManager pocketBases;
|
||||
private final Logger logger;
|
||||
private final Set<UUID> grantedFlight = new HashSet<>();
|
||||
private final Set<UUID> warned = new HashSet<>();
|
||||
|
||||
@@ -25,12 +35,60 @@ final class BaseFlightController implements Runnable {
|
||||
SecondaryProgressionService progressionService,
|
||||
BaseBoundsService boundsService,
|
||||
PluginSettingsProvider settings
|
||||
) {
|
||||
this(server, stateManager, progressionService, boundsService, settings, null,
|
||||
Logger.getLogger(BaseFlightController.class.getName()));
|
||||
}
|
||||
|
||||
BaseFlightController(
|
||||
Server server,
|
||||
BaseStateManager stateManager,
|
||||
SecondaryProgressionService progressionService,
|
||||
BaseBoundsService boundsService,
|
||||
PluginSettingsProvider settings,
|
||||
PocketBaseManager pocketBases,
|
||||
Logger logger
|
||||
) {
|
||||
this.server = server;
|
||||
this.stateManager = stateManager;
|
||||
this.progressionService = progressionService;
|
||||
this.boundsService = boundsService;
|
||||
this.settings = settings;
|
||||
this.pocketBases = pocketBases;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRaidFinish(RaidFinishEvent event) {
|
||||
if (pocketBases == null) {
|
||||
return;
|
||||
}
|
||||
Optional<UUID> owner = pocketBases.ownerForPocketWorld(event.getWorld().getUID());
|
||||
if (owner.isEmpty() || event.getWinners().stream().noneMatch(
|
||||
winner -> winner.getUniqueId().equals(owner.orElseThrow()))) {
|
||||
return;
|
||||
}
|
||||
UUID ownerId = owner.orElseThrow();
|
||||
if (pocketBases.state(ownerId).flightUnlocked()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
pocketBases.unlockFlight(ownerId);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
logger.log(Level.SEVERE, "Could not unlock Pocket Base flight", exception);
|
||||
return;
|
||||
}
|
||||
event.getWinners().stream()
|
||||
.filter(winner -> winner.getUniqueId().equals(ownerId))
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.sendTitle(
|
||||
ChatColor.GOLD + "Pocket Base Flight Unlocked",
|
||||
ChatColor.YELLOW + "Flight is now available in your Pocket Base",
|
||||
settings.current().titleFadeInTicks(),
|
||||
settings.current().titleStayTicks(),
|
||||
settings.current().titleFadeOutTicks()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,8 +147,20 @@ final class BaseFlightController implements Runnable {
|
||||
}
|
||||
|
||||
private void applyFlight(Player player, PlayerState state) {
|
||||
if (player.getGameMode() != GameMode.SURVIVAL
|
||||
|| !state.flightEnabled() || state.flightLevel() < 1 || state.base().isEmpty()) {
|
||||
if (player.getGameMode() != GameMode.SURVIVAL) {
|
||||
removeGrantedFlight(player);
|
||||
return;
|
||||
}
|
||||
if (pocketBases != null) {
|
||||
Optional<UUID> pocketOwner = pocketBases.ownerForPocketWorld(
|
||||
player.getWorld().getUID()
|
||||
);
|
||||
if (pocketOwner.isPresent()) {
|
||||
applyPocketFlight(player, pocketOwner.orElseThrow());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!state.flightEnabled() || state.flightLevel() < 1 || state.base().isEmpty()) {
|
||||
removeGrantedFlight(player);
|
||||
return;
|
||||
}
|
||||
@@ -127,6 +197,43 @@ final class BaseFlightController implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPocketFlight(Player player, UUID ownerId) {
|
||||
PocketBaseState pocket = pocketBases.state(ownerId);
|
||||
if (!ownerId.equals(player.getUniqueId()) || !pocket.flightUnlocked()
|
||||
|| pocket.level() < 1) {
|
||||
removeGrantedFlight(player);
|
||||
return;
|
||||
}
|
||||
int halfSize = pocketBases.policy().size(pocket.level()) / 2;
|
||||
int x = player.getLocation().getBlockX();
|
||||
int z = player.getLocation().getBlockZ();
|
||||
if (x < -halfSize - POCKET_FLIGHT_BUFFER
|
||||
|| x >= halfSize + POCKET_FLIGHT_BUFFER
|
||||
|| z < -halfSize - POCKET_FLIGHT_BUFFER
|
||||
|| z >= halfSize + POCKET_FLIGHT_BUFFER) {
|
||||
removeGrantedFlight(player);
|
||||
return;
|
||||
}
|
||||
if (!player.getAllowFlight()) {
|
||||
player.setAllowFlight(true);
|
||||
grantedFlight.add(player.getUniqueId());
|
||||
}
|
||||
boolean inBuffer = !pocketBases.policy().contains(pocket.level(), x, z);
|
||||
if (inBuffer && player.isFlying()) {
|
||||
if (warned.add(player.getUniqueId())) {
|
||||
player.sendTitle(
|
||||
ChatColor.RED + "Leaving Your Pocket Base",
|
||||
ChatColor.YELLOW + "Turn back before Pocket Base flight ends",
|
||||
0,
|
||||
Math.min(30, settings.current().titleStayTicks()),
|
||||
settings.current().titleFadeOutTicks()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warned.remove(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean withinVerticalRange(Player player, PlayerState state, BaseLocation base) {
|
||||
int y = player.getLocation().getBlockY();
|
||||
if (state.flightLevel() == 3) {
|
||||
|
||||
@@ -565,7 +565,9 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
+ "; hostile spawning="
|
||||
+ (pocket.hostileMobSpawningEnabled() ? "enabled" : "disabled")
|
||||
+ "; passive spawning="
|
||||
+ (pocket.passiveMobSpawningEnabled() ? "enabled" : "disabled"));
|
||||
+ (pocket.passiveMobSpawningEnabled() ? "enabled" : "disabled")
|
||||
+ "; flight="
|
||||
+ (pocket.flightUnlocked() ? "unlocked" : "complete a raid to unlock"));
|
||||
}
|
||||
|
||||
private void showCooldownPath(Player player, PlayerState state) {
|
||||
|
||||
@@ -159,7 +159,10 @@ final class BaseSettingsDialogFactory {
|
||||
: "Biome: " + pocket.biome().worldType().commandName() + "/"
|
||||
+ pocket.biome().commandName() + "\nHostile mobs: "
|
||||
+ enabled(pocket.hostileMobSpawningEnabled()) + " • Passive mobs: "
|
||||
+ enabled(pocket.passiveMobSpawningEnabled())
|
||||
+ enabled(pocket.passiveMobSpawningEnabled()) + "\nFlight: "
|
||||
+ (pocket.flightUnlocked()
|
||||
? "Unlocked"
|
||||
: "Complete a raid to unlock")
|
||||
),
|
||||
dialogs,
|
||||
1,
|
||||
|
||||
@@ -92,6 +92,17 @@ final class PocketBaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
PocketBaseState unlockFlight(UUID ownerId) throws IOException {
|
||||
PocketBaseState current = states.state(ownerId);
|
||||
if (current.level() < 1) {
|
||||
throw new IllegalStateException("Pocket Base I is still locked");
|
||||
}
|
||||
if (current.flightUnlocked()) {
|
||||
return current;
|
||||
}
|
||||
return states.updateAndSave(ownerId, PocketBaseState::withFlightUnlocked);
|
||||
}
|
||||
|
||||
PocketBaseState setHostileMobSpawning(UUID ownerId, boolean enabled)
|
||||
throws IOException {
|
||||
PocketBaseState current = states.state(ownerId);
|
||||
|
||||
@@ -10,7 +10,8 @@ public record PocketBaseState(
|
||||
Optional<PocketPortalLocation> returnPortal,
|
||||
boolean hostileMobSpawningEnabled,
|
||||
boolean passiveMobSpawningEnabled,
|
||||
PocketBaseBiome biome
|
||||
PocketBaseBiome biome,
|
||||
boolean flightUnlocked
|
||||
) {
|
||||
public PocketBaseState {
|
||||
if (ownerId == null) {
|
||||
@@ -27,6 +28,21 @@ public record PocketBaseState(
|
||||
}
|
||||
}
|
||||
|
||||
public PocketBaseState(
|
||||
UUID ownerId,
|
||||
int level,
|
||||
Optional<PocketPortalLocation> entrance,
|
||||
Optional<PocketPortalLocation> returnPortal,
|
||||
boolean hostileMobSpawningEnabled,
|
||||
boolean passiveMobSpawningEnabled,
|
||||
PocketBaseBiome biome
|
||||
) {
|
||||
this(
|
||||
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, false
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState(
|
||||
UUID ownerId,
|
||||
int level,
|
||||
@@ -34,63 +50,70 @@ public record PocketBaseState(
|
||||
) {
|
||||
this(
|
||||
ownerId, level, entrance, Optional.empty(), false, false,
|
||||
PocketBaseBiome.THE_VOID
|
||||
PocketBaseBiome.THE_VOID, false
|
||||
);
|
||||
}
|
||||
|
||||
public static PocketBaseState locked(UUID ownerId) {
|
||||
return new PocketBaseState(
|
||||
ownerId, 0, Optional.empty(), Optional.empty(), false, false,
|
||||
PocketBaseBiome.THE_VOID
|
||||
PocketBaseBiome.THE_VOID, false
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState withLevel(int newLevel) {
|
||||
return new PocketBaseState(
|
||||
ownerId, newLevel, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState withEntrance(PocketPortalLocation portal) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, Optional.of(portal), returnPortal,
|
||||
hostileMobSpawningEnabled, passiveMobSpawningEnabled, biome
|
||||
hostileMobSpawningEnabled, passiveMobSpawningEnabled, biome, flightUnlocked
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState withoutEntrance() {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, Optional.empty(), returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState withReturnPortal(PocketPortalLocation portal) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, entrance, Optional.of(portal), hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState withHostileMobSpawningEnabled(boolean enabled) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, entrance, returnPortal, enabled,
|
||||
passiveMobSpawningEnabled, biome
|
||||
passiveMobSpawningEnabled, biome, flightUnlocked
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState withPassiveMobSpawningEnabled(boolean enabled) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
enabled, biome
|
||||
enabled, biome, flightUnlocked
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState withBiome(PocketBaseBiome selectedBiome) {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, selectedBiome
|
||||
passiveMobSpawningEnabled, selectedBiome, flightUnlocked
|
||||
);
|
||||
}
|
||||
|
||||
public PocketBaseState withFlightUnlocked() {
|
||||
return new PocketBaseState(
|
||||
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||
passiveMobSpawningEnabled, biome, true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,8 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
settingsProvider
|
||||
);
|
||||
flightController = new BaseFlightController(
|
||||
getServer(), stateManager, secondaryProgressionService, boundsService, settingsProvider
|
||||
getServer(), stateManager, secondaryProgressionService, boundsService,
|
||||
settingsProvider, pocketBaseManager, getLogger()
|
||||
);
|
||||
VisitorPolicy visitorPolicy = new VisitorPolicy();
|
||||
teleportManager = new BaseTeleportManager(
|
||||
@@ -84,6 +85,7 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
getLogger()
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(progressListener, this);
|
||||
getServer().getPluginManager().registerEvents(flightController, this);
|
||||
getServer().getPluginManager().registerEvents(teleportManager, this);
|
||||
getServer().getPluginManager().registerEvents(pocketBaseController, this);
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ public final class YamlPocketBaseRepository {
|
||||
loadPortal(yaml, path + ".return-portal"),
|
||||
loadMobPreference(yaml, path, "hostile-mob-spawning-enabled"),
|
||||
loadMobPreference(yaml, path, "passive-mob-spawning-enabled"),
|
||||
loadBiome(yaml, path)
|
||||
loadBiome(yaml, path),
|
||||
yaml.getBoolean(path + ".flight-unlocked", false)
|
||||
);
|
||||
states.put(ownerId, state);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
@@ -73,6 +74,7 @@ public final class YamlPocketBaseRepository {
|
||||
path + ".passive-mob-spawning-enabled",
|
||||
state.passiveMobSpawningEnabled()
|
||||
);
|
||||
yaml.set(path + ".flight-unlocked", state.flightUnlocked());
|
||||
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));
|
||||
|
||||
@@ -12,11 +12,14 @@ import static org.mockito.Mockito.when;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.raid.RaidFinishEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -47,6 +50,7 @@ final class BaseFlightControllerTest {
|
||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
|
||||
doReturn(List.of(player)).when(server).getOnlinePlayers();
|
||||
when(server.getPlayer(playerId)).thenReturn(player);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Alex");
|
||||
when(player.getGameMode()).thenReturn(org.bukkit.GameMode.SURVIVAL);
|
||||
@@ -62,6 +66,219 @@ final class BaseFlightControllerTest {
|
||||
when(player.getAllowFlight()).thenReturn(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ownerWinningRaidInOwnPocketBaseUnlocksFlight() throws Exception {
|
||||
UUID pocketWorldId = UUID.randomUUID();
|
||||
World pocketWorld = mock(World.class);
|
||||
Player winner = mock(Player.class);
|
||||
RaidFinishEvent event = mock(RaidFinishEvent.class);
|
||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||
PocketBaseState locked = new PocketBaseState(playerId, 1, Optional.empty());
|
||||
PocketBaseState unlocked = locked.withFlightUnlocked();
|
||||
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(event.getWorld()).thenReturn(pocketWorld);
|
||||
when(event.getWinners()).thenReturn(List.of(winner));
|
||||
when(pocketWorld.getUID()).thenReturn(pocketWorldId);
|
||||
when(winner.getUniqueId()).thenReturn(playerId);
|
||||
when(pocketBases.ownerForPocketWorld(pocketWorldId)).thenReturn(Optional.of(playerId));
|
||||
when(pocketBases.state(playerId)).thenReturn(locked);
|
||||
when(pocketBases.unlockFlight(playerId)).thenReturn(unlocked);
|
||||
|
||||
pocketController.onRaidFinish(event);
|
||||
|
||||
verify(pocketBases).unlockFlight(playerId);
|
||||
verify(winner).sendTitle(anyString(), anyString(), anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void raidDoesNotUnlockFlightWhenPocketOwnerIsNotAWinner() throws Exception {
|
||||
UUID pocketWorldId = UUID.randomUUID();
|
||||
UUID ownerId = UUID.randomUUID();
|
||||
World pocketWorld = mock(World.class);
|
||||
Player visitor = mock(Player.class);
|
||||
RaidFinishEvent event = mock(RaidFinishEvent.class);
|
||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||
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(event.getWorld()).thenReturn(pocketWorld);
|
||||
when(event.getWinners()).thenReturn(List.of(visitor));
|
||||
when(pocketWorld.getUID()).thenReturn(pocketWorldId);
|
||||
when(visitor.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(pocketBases.ownerForPocketWorld(pocketWorldId)).thenReturn(Optional.of(ownerId));
|
||||
|
||||
pocketController.onRaidFinish(event);
|
||||
|
||||
verify(pocketBases, never()).unlockFlight(ownerId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unlockedOwnerReceivesFlightInsidePocketBaseBuffer() {
|
||||
UUID pocketWorldId = UUID.randomUUID();
|
||||
World pocketWorld = mock(World.class);
|
||||
Location location = mock(Location.class);
|
||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||
PocketBaseState pocket = new PocketBaseState(playerId, 1, Optional.empty())
|
||||
.withFlightUnlocked();
|
||||
PlayerState noNormalFlight = PlayerState.newPlayer(playerId, "Alex");
|
||||
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(noNormalFlight);
|
||||
when(player.getWorld()).thenReturn(pocketWorld);
|
||||
when(player.getLocation()).thenReturn(location);
|
||||
when(player.getAllowFlight()).thenReturn(false);
|
||||
when(pocketWorld.getUID()).thenReturn(pocketWorldId);
|
||||
when(location.getX()).thenReturn(40.5);
|
||||
when(location.getZ()).thenReturn(0.5);
|
||||
when(location.getBlockX()).thenReturn(40);
|
||||
when(location.getBlockZ()).thenReturn(0);
|
||||
when(pocketBases.ownerForPocketWorld(pocketWorldId)).thenReturn(Optional.of(playerId));
|
||||
when(pocketBases.state(playerId)).thenReturn(pocket);
|
||||
when(pocketBases.policy()).thenReturn(
|
||||
new PocketBasePolicy(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
pocketController.run();
|
||||
|
||||
verify(player).setAllowFlight(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void flyingInPocketBaseBufferShowsLeavingWarning() {
|
||||
UUID pocketWorldId = UUID.randomUUID();
|
||||
World pocketWorld = mock(World.class);
|
||||
Location location = mock(Location.class);
|
||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||
PocketBaseState pocket = new PocketBaseState(playerId, 1, Optional.empty())
|
||||
.withFlightUnlocked();
|
||||
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.isFlying()).thenReturn(true);
|
||||
when(pocketWorld.getUID()).thenReturn(pocketWorldId);
|
||||
when(location.getBlockX()).thenReturn(40);
|
||||
when(location.getBlockZ()).thenReturn(0);
|
||||
when(pocketBases.ownerForPocketWorld(pocketWorldId)).thenReturn(Optional.of(playerId));
|
||||
when(pocketBases.state(playerId)).thenReturn(pocket);
|
||||
when(pocketBases.policy()).thenReturn(
|
||||
new PocketBasePolicy(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
pocketController.run();
|
||||
|
||||
verify(player).sendTitle(anyString(), anyString(), anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pocketBaseFlightEndsBeyondSixteenBlockBuffer() {
|
||||
UUID pocketWorldId = UUID.randomUUID();
|
||||
World pocketWorld = mock(World.class);
|
||||
Location location = mock(Location.class);
|
||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||
PocketBaseState pocket = new PocketBaseState(playerId, 1, Optional.empty())
|
||||
.withFlightUnlocked();
|
||||
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(location.getBlockX()).thenReturn(40, 48);
|
||||
when(location.getBlockZ()).thenReturn(0);
|
||||
when(pocketBases.ownerForPocketWorld(pocketWorldId)).thenReturn(Optional.of(playerId));
|
||||
when(pocketBases.state(playerId)).thenReturn(pocket);
|
||||
when(pocketBases.policy()).thenReturn(
|
||||
new PocketBasePolicy(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
pocketController.run();
|
||||
pocketController.run();
|
||||
|
||||
verify(player).setFlying(false);
|
||||
verify(player).setAllowFlight(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void visitorDoesNotReceiveUnlockedOwnersPocketBaseFlight() {
|
||||
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();
|
||||
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);
|
||||
|
||||
pocketController.run();
|
||||
|
||||
verify(player, never()).setAllowFlight(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void walkingInWarningBufferDoesNotShowLeavingBaseWarning() {
|
||||
when(player.isFlying()).thenReturn(false);
|
||||
|
||||
@@ -489,6 +489,7 @@ final class BaseSettingsCommandTest {
|
||||
&& message.contains("type=void/the_void")
|
||||
&& message.contains("hostile spawning=enabled")
|
||||
&& message.contains("passive spawning=disabled")
|
||||
&& message.contains("flight=complete a raid to unlock")
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,13 @@ final class BaseSettingsDialogFactoryTest {
|
||||
specification
|
||||
);
|
||||
assertTrue(root.content().message().contains("Base 4/4"));
|
||||
BaseSettingsDialogFactory.ListSpec pocketSettings = assertInstanceOf(
|
||||
BaseSettingsDialogFactory.ListSpec.class,
|
||||
root.dialogs().get(3)
|
||||
);
|
||||
assertTrue(pocketSettings.content().message().contains(
|
||||
"Flight: Complete a raid to unlock"
|
||||
));
|
||||
List<String> commands = commands(root);
|
||||
assertTrue(commands.contains("/basesettings status"));
|
||||
assertTrue(commands.contains("/basesettings visitors blocked"));
|
||||
|
||||
@@ -2,6 +2,7 @@ package games.dmg.spigotbase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -58,6 +59,32 @@ final class PocketBaseManagerTest {
|
||||
assertEquals(generated, manager.returnPortal(ownerId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistsPocketBaseFlightUnlock() throws Exception {
|
||||
UUID ownerId = UUID.randomUUID();
|
||||
PocketBaseState current = new PocketBaseState(ownerId, 1, Optional.empty());
|
||||
PocketBaseStateManager states = mock(PocketBaseStateManager.class);
|
||||
when(states.state(ownerId)).thenReturn(current);
|
||||
when(states.updateAndSave(org.mockito.ArgumentMatchers.eq(ownerId),
|
||||
org.mockito.ArgumentMatchers.any())).thenAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
UnaryOperator<PocketBaseState> operation = invocation.getArgument(1);
|
||||
return operation.apply(current);
|
||||
});
|
||||
PocketBaseManager manager = new PocketBaseManager(
|
||||
states,
|
||||
mock(PocketBaseWorldService.class),
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
PocketBaseState updated = manager.unlockFlight(ownerId);
|
||||
|
||||
assertTrue(updated.flightUnlocked());
|
||||
verify(states).updateAndSave(
|
||||
org.mockito.ArgumentMatchers.eq(ownerId), org.mockito.ArgumentMatchers.any()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistsAndAppliesHostileMobSpawningWithoutChangingPassive() throws Exception {
|
||||
UUID ownerId = UUID.randomUUID();
|
||||
|
||||
@@ -31,7 +31,8 @@ final class YamlPocketBaseRepositoryTest {
|
||||
)),
|
||||
true,
|
||||
false,
|
||||
PocketBaseBiome.CRIMSON_FOREST
|
||||
PocketBaseBiome.CRIMSON_FOREST,
|
||||
true
|
||||
);
|
||||
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(
|
||||
temporaryDirectory.resolve("pocket-bases.yml")
|
||||
@@ -55,6 +56,7 @@ final class YamlPocketBaseRepositoryTest {
|
||||
assertFalse(loaded.passiveMobSpawningEnabled());
|
||||
assertTrue(loaded.returnPortal().isEmpty());
|
||||
assertEquals(PocketBaseBiome.THE_VOID, loaded.biome());
|
||||
assertFalse(loaded.flightUnlocked());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user