diff --git a/README.md b/README.md index aa118ff..dbaea15 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ The plugin JAR is written to `build/libs/`. /basesettings upgrade /basesettings pocket upgrade /basesettings pocket mobs +/basesettings pocket type /basesettings visitors /basesettings navigation /basesettings flight @@ -46,6 +47,8 @@ After 250 Survival-mode block placements anywhere by default, the spawnable over 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. 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 and can be controlled by the owner with `/basesettings pocket mobs `. +Owners can change biome metadata without altering Pocket Base blocks, entities, inventories, or portals by using `/basesettings pocket type `. 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. + ## Administration The `spigotbase.admin` permission is granted to server operators by default. `/homeadmin` aliases `/baseadmin`. diff --git a/design/log.md b/design/log.md index 372fe11..22ea55d 100644 --- a/design/log.md +++ b/design/log.md @@ -105,3 +105,11 @@ description: Chronological record of material decisions affecting the Spigot Bas - Persisted one active return portal location while retaining the generated portal as the default for existing Pocket Bases. - Made only the selected intact frame functional without removing old frame blocks, preserving the void-return safety fallback. - Verified the feature with `./gradlew clean check jar`. + +## 2026-08-24 — Purchasable Pocket Base biomes + +- Added `/basesettings pocket type ` with type-aware autocomplete and validation. +- Defaulted new and unconfigured Pocket Bases to `void/the_void` and persisted each owner's selected biome. +- Applied biome metadata across unlocked columns and new expansion rings without changing blocks, entities, inventories, portals, progression, or mob-spawning preferences. +- Added an atomic, configurable biome-change payment that defaults to 16 netherite blocks and restores the prior biome and payment on failure. +- Verified the feature with `./gradlew clean check jar`. diff --git a/design/user-stories/index.md b/design/user-stories/index.md index 3de5763..51788c2 100644 --- a/design/user-stories/index.md +++ b/design/user-stories/index.md @@ -24,3 +24,4 @@ description: Catalog of user stories for the Spigot Base plugin. 16. [US-016: Build and use Pocket Base portals](us-016-build-and-use-pocket-base-portals.md) 17. [US-017: Expand a Pocket Base](us-017-expand-a-pocket-base.md) 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) diff --git a/design/user-stories/us-018-control-pocket-base-mob-spawning.md b/design/user-stories/us-018-control-pocket-base-mob-spawning.md index 7c1d4d2..360220d 100644 --- a/design/user-stories/us-018-control-pocket-base-mob-spawning.md +++ b/design/user-stories/us-018-control-pocket-base-mob-spawning.md @@ -26,3 +26,4 @@ As a **Pocket Base owner**, I want to enable or disable natural mob spawning in - [US-015: Unlock a Pocket Base](us-015-unlock-a-pocket-base.md) - [US-016: Build and use Pocket Base portals](us-016-build-and-use-pocket-base-portals.md) - [US-017: Expand a Pocket Base](us-017-expand-a-pocket-base.md) +- [US-019: Change a Pocket Base biome](us-019-change-a-pocket-base-biome.md) diff --git a/design/user-stories/us-019-change-a-pocket-base-biome.md b/design/user-stories/us-019-change-a-pocket-base-biome.md new file mode 100644 index 0000000..2ffc33a --- /dev/null +++ b/design/user-stories/us-019-change-a-pocket-base-biome.md @@ -0,0 +1,31 @@ +--- +type: User Story +title: "US-019: Change a Pocket Base biome" +description: Let Pocket Base owners purchase biome changes that alter natural mob selection without changing their builds. +status: done +--- + +# US-019: Change a Pocket Base biome + +As a **Pocket Base owner**, I want to change my Pocket Base biome so that I can control its biome-specific atmosphere and natural mob selection without rebuilding it. + +## Acceptance criteria + +- [x] A Pocket Base owner can use `/basesettings pocket type ` to select a compatible biome. +- [x] Void supports `the_void`; Nether supports its vanilla biome subtypes; and Overworld supports its vanilla biome subtypes. +- [x] New and previously unconfigured Pocket Bases default to the void type and `the_void` subtype. +- [x] Each change atomically charges a configurable currency and amount that default to 16 netherite blocks. +- [x] Selecting the active subtype or providing an invalid type/subtype combination is rejected without charging. +- [x] Changing subtypes within the same type incurs the configured cost. +- [x] The selected biome is applied across every unlocked Pocket Base column without changing blocks, entities, inventories, portals, level, or the mob-spawning preference. +- [x] Newly unlocked terrain receives the selected biome. +- [x] When natural mob spawning is enabled, Minecraft can use the selected biome for applicable natural mob selection. +- [x] The selected type and subtype persist through restarts and appear in `/basesettings status`. +- [x] The command, types, and compatible subtypes are offered through contextual autocomplete. +- [x] Failed biome application or persistence restores the prior biome and does not consume payment. + +## Related + +- [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-018: Control Pocket Base mob spawning](us-018-control-pocket-base-mob-spawning.md) diff --git a/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java b/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java index 35f3b96..caa13f2 100644 --- a/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java +++ b/src/main/java/games/dmg/spigotbase/BaseSettingsCommand.java @@ -20,6 +20,9 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { ); private static final List VISITOR_MODES = List.of("allowed", "blocked"); private static final List ENABLE_MODES = List.of("enable", "disable"); + private static final List POCKET_WORLD_TYPES = List.of( + "void", "nether", "overworld" + ); private final BaseStateManager stateManager; private final PluginSettingsProvider settings; private final TeleportPolicy teleportPolicy; @@ -70,6 +73,10 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { && arguments[1].equalsIgnoreCase("mobs")) { return updatePocketMobSpawning(player, arguments[2]); } + if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket") + && arguments[1].equalsIgnoreCase("type")) { + return purchasePocketBiome(player, arguments[2], arguments[3]); + } if (arguments.length == 2 && arguments[0].equalsIgnoreCase("visitors")) { return updateVisitors(player, state, arguments[1]); } @@ -180,6 +187,59 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { return true; } + private boolean purchasePocketBiome( + Player player, + String worldTypeName, + String biomeName + ) { + if (pocketBases == null) { + player.sendMessage(ChatColor.RED + "Pocket Bases are currently unavailable."); + return true; + } + PocketBaseState current = pocketBases.state(player.getUniqueId()); + if (current.level() < 1) { + player.sendMessage(ChatColor.RED + "Pocket Base I is still locked."); + return true; + } + PocketBaseBiome biome = PocketBaseWorldType.fromCommand(worldTypeName) + .flatMap(worldType -> PocketBaseBiome.fromCommand(worldType, biomeName)) + .orElse(null); + if (biome == null) { + player.sendMessage(ChatColor.RED + "That Pocket Base type and subtype are invalid."); + return true; + } + if (biome == current.biome()) { + player.sendMessage(ChatColor.RED + "Your Pocket Base already uses " + + biome.commandName() + "."); + return true; + } + int price = settings.current().pocketBaseBiomeChangeCost(); + Material currency = Material.valueOf( + settings.current().pocketBaseBiomeCurrencyMaterial() + ); + PlayerInventory inventory = player.getInventory(); + if (countCurrency(inventory, currency) < price) { + player.sendMessage(ChatColor.RED + "Changing Pocket Base biome costs " + + price + " " + currency.name().toLowerCase(Locale.ROOT) + "."); + return true; + } + ItemStack[] snapshot = cloneContents(inventory.getStorageContents()); + removeCurrency(inventory, currency, price); + final PocketBaseState updated; + try { + updated = pocketBases.setBiome(player.getUniqueId(), biome); + } catch (IOException | RuntimeException exception) { + inventory.setStorageContents(snapshot); + player.sendMessage(ChatColor.RED + + "The Pocket Base biome change failed; your payment was restored."); + return true; + } + player.sendMessage(ChatColor.GREEN + "Pocket Base biome changed to " + + updated.biome().commandName() + " for " + price + " " + + currency.name().toLowerCase(Locale.ROOT) + "."); + return true; + } + private boolean updatePocketMobSpawning(Player player, String mode) { if (pocketBases == null) { player.sendMessage(ChatColor.RED + "Pocket Bases are currently unavailable."); @@ -452,7 +512,9 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { player.sendMessage(colorForPrerequisite(owner.baseLevel() >= 4 && owner.base().isPresent()) + "Pocket Base " + pocket.level() + ": " + ChatColor.GRAY + price + " " + settings.current().pocketBaseCurrencyMaterial().toLowerCase(Locale.ROOT) - + " → /basesettings pocket upgrade; mob spawning=" + + " → /basesettings pocket upgrade; type=" + + pocket.biome().worldType().commandName() + "/" + pocket.biome().commandName() + + "; mob spawning=" + (pocket.mobSpawningEnabled() ? "enabled" : "disabled")); } @@ -485,17 +547,33 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { List modes = arguments[0].equalsIgnoreCase("visitors") ? VISITOR_MODES : switch (arguments[0].toLowerCase(Locale.ROOT)) { - case "pocket" -> List.of("upgrade", "mobs"); + case "pocket" -> List.of("upgrade", "mobs", "type"); case "navigation", "flight", "border", "spawnable", "bossbar" -> ENABLE_MODES; default -> List.of(); }; String prefix = arguments[1].toLowerCase(Locale.ROOT); return modes.stream().filter(mode -> mode.startsWith(prefix)).toList(); } - if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket") - && arguments[1].equalsIgnoreCase("mobs")) { + if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket")) { String prefix = arguments[2].toLowerCase(Locale.ROOT); - return ENABLE_MODES.stream().filter(mode -> mode.startsWith(prefix)).toList(); + if (arguments[1].equalsIgnoreCase("mobs")) { + return ENABLE_MODES.stream().filter(mode -> mode.startsWith(prefix)).toList(); + } + if (arguments[1].equalsIgnoreCase("type")) { + return POCKET_WORLD_TYPES.stream() + .filter(type -> type.startsWith(prefix)) + .toList(); + } + } + if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket") + && arguments[1].equalsIgnoreCase("type")) { + String prefix = arguments[3].toLowerCase(Locale.ROOT); + return PocketBaseWorldType.fromCommand(arguments[2]) + .map(PocketBaseBiome::commandNames) + .orElse(List.of()) + .stream() + .filter(biome -> biome.startsWith(prefix)) + .toList(); } return List.of(); } @@ -557,6 +635,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter { private static void sendUsage(Player player) { player.sendMessage(ChatColor.RED + "Usage: /basesettings " + "[status|upgrade|pocket upgrade|pocket mobs " + + "|pocket type " + "|visitors |navigation " + "|flight |border " + "|spawnable |bossbar ]"); diff --git a/src/main/java/games/dmg/spigotbase/PluginSettings.java b/src/main/java/games/dmg/spigotbase/PluginSettings.java index 92ae8e2..30af0f5 100644 --- a/src/main/java/games/dmg/spigotbase/PluginSettings.java +++ b/src/main/java/games/dmg/spigotbase/PluginSettings.java @@ -39,12 +39,14 @@ public record PluginSettings( int visitorUnlockDiamondCost, int pocketBaseUnlockCost, int pocketBaseUpgradeCost, + int pocketBaseBiomeChangeCost, Set baseUnlockMaterials, String stoneExpansionMaterial, String deepslateExpansionMaterial, String obsidianExpansionMaterial, String visitorCurrencyMaterial, String pocketBaseCurrencyMaterial, + String pocketBaseBiomeCurrencyMaterial, String pocketBasePortalFrameMaterial, Set cooldownExcludedMaterials, int bossBarDurationTicks, @@ -85,6 +87,7 @@ public record PluginSettings( private static final int DEFAULT_VISITOR_UNLOCK_DIAMOND_COST = 128; private static final int DEFAULT_POCKET_BASE_UNLOCK_COST = 64; private static final int DEFAULT_POCKET_BASE_UPGRADE_COST = 16; + private static final int DEFAULT_POCKET_BASE_BIOME_CHANGE_COST = 16; private static final int DEFAULT_BOSS_BAR_DURATION_TICKS = 60; private static final int DEFAULT_NAVIGATION_PARTICLE_COUNT = 5; private static final int DEFAULT_TITLE_FADE_IN_TICKS = 10; @@ -148,6 +151,7 @@ public record PluginSettings( requirePositive(visitorUnlockDiamondCost, "visitor-unlock-diamond-cost"); requirePositive(pocketBaseUnlockCost, "pocket-base-unlock-cost"); requirePositive(pocketBaseUpgradeCost, "pocket-base-upgrade-cost"); + requirePositive(pocketBaseBiomeChangeCost, "pocket-base-biome-change-cost"); baseUnlockMaterials = normalizedSet(baseUnlockMaterials, "base-unlock-materials", false); stoneExpansionMaterial = normalizedName(stoneExpansionMaterial, "stone-expansion-material"); deepslateExpansionMaterial = normalizedName( @@ -162,6 +166,9 @@ public record PluginSettings( pocketBaseCurrencyMaterial = normalizedName( pocketBaseCurrencyMaterial, "pocket-base-currency-material" ); + pocketBaseBiomeCurrencyMaterial = normalizedName( + pocketBaseBiomeCurrencyMaterial, "pocket-base-biome-currency-material" + ); pocketBasePortalFrameMaterial = normalizedName( pocketBasePortalFrameMaterial, "pocket-base-portal-frame-material" ); @@ -230,12 +237,18 @@ public record PluginSettings( integer(values, "visitor-unlock-diamond-cost", DEFAULT_VISITOR_UNLOCK_DIAMOND_COST), integer(values, "pocket-base-unlock-cost", DEFAULT_POCKET_BASE_UNLOCK_COST), integer(values, "pocket-base-upgrade-cost", DEFAULT_POCKET_BASE_UPGRADE_COST), + integer( + values, + "pocket-base-biome-change-cost", + DEFAULT_POCKET_BASE_BIOME_CHANGE_COST + ), stringSet(values, "base-unlock-materials", Set.of("GRASS_BLOCK", "DIRT")), string(values, "stone-expansion-material", "STONE"), string(values, "deepslate-expansion-material", "DEEPSLATE"), string(values, "obsidian-expansion-material", "OBSIDIAN"), string(values, "visitor-currency-material", "DIAMOND"), string(values, "pocket-base-currency-material", "DIAMOND_BLOCK"), + string(values, "pocket-base-biome-currency-material", "NETHERITE_BLOCK"), string(values, "pocket-base-portal-frame-material", "DIAMOND_BLOCK"), stringSet(values, "cooldown-excluded-materials", Set.of()), integer(values, "boss-bar-duration-ticks", DEFAULT_BOSS_BAR_DURATION_TICKS), diff --git a/src/main/java/games/dmg/spigotbase/PluginSettingsValidator.java b/src/main/java/games/dmg/spigotbase/PluginSettingsValidator.java index 6bf40d9..4255177 100644 --- a/src/main/java/games/dmg/spigotbase/PluginSettingsValidator.java +++ b/src/main/java/games/dmg/spigotbase/PluginSettingsValidator.java @@ -29,6 +29,10 @@ final class PluginSettingsValidator { } validateCurrency(settings.visitorCurrencyMaterial(), "visitor-currency-material"); validateCurrency(settings.pocketBaseCurrencyMaterial(), "pocket-base-currency-material"); + validateCurrency( + settings.pocketBaseBiomeCurrencyMaterial(), + "pocket-base-biome-currency-material" + ); return settings; } diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseBiome.java b/src/main/java/games/dmg/spigotbase/PocketBaseBiome.java new file mode 100644 index 0000000..21af78b --- /dev/null +++ b/src/main/java/games/dmg/spigotbase/PocketBaseBiome.java @@ -0,0 +1,105 @@ +package games.dmg.spigotbase; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +public enum PocketBaseBiome { + THE_VOID(PocketBaseWorldType.VOID), + + NETHER_WASTES(PocketBaseWorldType.NETHER), + SOUL_SAND_VALLEY(PocketBaseWorldType.NETHER), + CRIMSON_FOREST(PocketBaseWorldType.NETHER), + WARPED_FOREST(PocketBaseWorldType.NETHER), + BASALT_DELTAS(PocketBaseWorldType.NETHER), + + OCEAN(PocketBaseWorldType.OVERWORLD), + PLAINS(PocketBaseWorldType.OVERWORLD), + DESERT(PocketBaseWorldType.OVERWORLD), + WINDSWEPT_HILLS(PocketBaseWorldType.OVERWORLD), + FOREST(PocketBaseWorldType.OVERWORLD), + TAIGA(PocketBaseWorldType.OVERWORLD), + SWAMP(PocketBaseWorldType.OVERWORLD), + MANGROVE_SWAMP(PocketBaseWorldType.OVERWORLD), + RIVER(PocketBaseWorldType.OVERWORLD), + FROZEN_OCEAN(PocketBaseWorldType.OVERWORLD), + FROZEN_RIVER(PocketBaseWorldType.OVERWORLD), + SNOWY_PLAINS(PocketBaseWorldType.OVERWORLD), + MUSHROOM_FIELDS(PocketBaseWorldType.OVERWORLD), + BEACH(PocketBaseWorldType.OVERWORLD), + JUNGLE(PocketBaseWorldType.OVERWORLD), + SPARSE_JUNGLE(PocketBaseWorldType.OVERWORLD), + DEEP_OCEAN(PocketBaseWorldType.OVERWORLD), + STONY_SHORE(PocketBaseWorldType.OVERWORLD), + SNOWY_BEACH(PocketBaseWorldType.OVERWORLD), + BIRCH_FOREST(PocketBaseWorldType.OVERWORLD), + DARK_FOREST(PocketBaseWorldType.OVERWORLD), + PALE_GARDEN(PocketBaseWorldType.OVERWORLD), + SNOWY_TAIGA(PocketBaseWorldType.OVERWORLD), + OLD_GROWTH_PINE_TAIGA(PocketBaseWorldType.OVERWORLD), + WINDSWEPT_FOREST(PocketBaseWorldType.OVERWORLD), + SAVANNA(PocketBaseWorldType.OVERWORLD), + SAVANNA_PLATEAU(PocketBaseWorldType.OVERWORLD), + BADLANDS(PocketBaseWorldType.OVERWORLD), + WOODED_BADLANDS(PocketBaseWorldType.OVERWORLD), + WARM_OCEAN(PocketBaseWorldType.OVERWORLD), + LUKEWARM_OCEAN(PocketBaseWorldType.OVERWORLD), + COLD_OCEAN(PocketBaseWorldType.OVERWORLD), + DEEP_LUKEWARM_OCEAN(PocketBaseWorldType.OVERWORLD), + DEEP_COLD_OCEAN(PocketBaseWorldType.OVERWORLD), + DEEP_FROZEN_OCEAN(PocketBaseWorldType.OVERWORLD), + SUNFLOWER_PLAINS(PocketBaseWorldType.OVERWORLD), + WINDSWEPT_GRAVELLY_HILLS(PocketBaseWorldType.OVERWORLD), + FLOWER_FOREST(PocketBaseWorldType.OVERWORLD), + ICE_SPIKES(PocketBaseWorldType.OVERWORLD), + OLD_GROWTH_BIRCH_FOREST(PocketBaseWorldType.OVERWORLD), + OLD_GROWTH_SPRUCE_TAIGA(PocketBaseWorldType.OVERWORLD), + WINDSWEPT_SAVANNA(PocketBaseWorldType.OVERWORLD), + ERODED_BADLANDS(PocketBaseWorldType.OVERWORLD), + BAMBOO_JUNGLE(PocketBaseWorldType.OVERWORLD), + DRIPSTONE_CAVES(PocketBaseWorldType.OVERWORLD), + LUSH_CAVES(PocketBaseWorldType.OVERWORLD), + DEEP_DARK(PocketBaseWorldType.OVERWORLD), + SULFUR_CAVES(PocketBaseWorldType.OVERWORLD), + MEADOW(PocketBaseWorldType.OVERWORLD), + GROVE(PocketBaseWorldType.OVERWORLD), + SNOWY_SLOPES(PocketBaseWorldType.OVERWORLD), + FROZEN_PEAKS(PocketBaseWorldType.OVERWORLD), + JAGGED_PEAKS(PocketBaseWorldType.OVERWORLD), + STONY_PEAKS(PocketBaseWorldType.OVERWORLD), + CHERRY_GROVE(PocketBaseWorldType.OVERWORLD); + + private final PocketBaseWorldType worldType; + + PocketBaseBiome(PocketBaseWorldType worldType) { + this.worldType = worldType; + } + + public PocketBaseWorldType worldType() { + return worldType; + } + + public String commandName() { + return name().toLowerCase(Locale.ROOT); + } + + static Optional fromCommand( + PocketBaseWorldType worldType, + String value + ) { + try { + PocketBaseBiome biome = valueOf(value.toUpperCase(Locale.ROOT)); + return biome.worldType == worldType ? Optional.of(biome) : Optional.empty(); + } catch (IllegalArgumentException exception) { + return Optional.empty(); + } + } + + static List commandNames(PocketBaseWorldType worldType) { + return Arrays.stream(values()) + .filter(biome -> biome.worldType == worldType) + .map(PocketBaseBiome::commandName) + .toList(); + } +} diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseManager.java b/src/main/java/games/dmg/spigotbase/PocketBaseManager.java index 047b203..a915490 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseManager.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseManager.java @@ -38,7 +38,7 @@ final class PocketBaseManager { PocketBaseState current = states.state(ownerId); int nextLevel = Math.addExact(current.level(), 1); policy.size(nextLevel); - worlds.expand(ownerId, current.level(), nextLevel); + worlds.expand(ownerId, current.level(), nextLevel, current.biome()); return states.updateAndSave(ownerId, state -> state.withLevel(nextLevel)); } @@ -71,6 +71,27 @@ final class PocketBaseManager { return states.updateAndSave(ownerId, state -> state.withReturnPortal(portal)); } + PocketBaseState setBiome(UUID ownerId, PocketBaseBiome biome) throws IOException { + PocketBaseState current = states.state(ownerId); + if (current.level() < 1) { + throw new IllegalStateException("Pocket Base I is still locked"); + } + if (current.biome() == biome) { + throw new IllegalArgumentException("Pocket Base already uses that biome"); + } + try { + worlds.setBiome(ownerId, current.level(), biome); + return states.updateAndSave(ownerId, state -> state.withBiome(biome)); + } catch (IOException | RuntimeException exception) { + try { + worlds.setBiome(ownerId, current.level(), current.biome()); + } catch (RuntimeException rollbackFailure) { + exception.addSuppressed(rollbackFailure); + } + throw exception; + } + } + PocketBaseState setMobSpawning(UUID ownerId, boolean enabled) throws IOException { PocketBaseState current = states.state(ownerId); if (current.level() < 1) { diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseState.java b/src/main/java/games/dmg/spigotbase/PocketBaseState.java index 8cfad6c..1541379 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseState.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseState.java @@ -8,7 +8,8 @@ public record PocketBaseState( int level, Optional entrance, Optional returnPortal, - boolean mobSpawningEnabled + boolean mobSpawningEnabled, + PocketBaseBiome biome ) { public PocketBaseState { if (ownerId == null) { @@ -19,6 +20,7 @@ public record PocketBaseState( } entrance = entrance == null ? Optional.empty() : entrance; returnPortal = returnPortal == null ? Optional.empty() : returnPortal; + 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"); } @@ -29,7 +31,9 @@ public record PocketBaseState( int level, Optional entrance ) { - this(ownerId, level, entrance, Optional.empty(), false); + this( + ownerId, level, entrance, Optional.empty(), false, PocketBaseBiome.THE_VOID + ); } public PocketBaseState( @@ -38,40 +42,63 @@ public record PocketBaseState( Optional entrance, boolean mobSpawningEnabled ) { - this(ownerId, level, entrance, Optional.empty(), mobSpawningEnabled); + this( + ownerId, level, entrance, Optional.empty(), mobSpawningEnabled, + PocketBaseBiome.THE_VOID + ); + } + + public PocketBaseState( + UUID ownerId, + int level, + Optional entrance, + Optional returnPortal, + boolean mobSpawningEnabled + ) { + this( + ownerId, level, entrance, returnPortal, mobSpawningEnabled, + PocketBaseBiome.THE_VOID + ); } public static PocketBaseState locked(UUID ownerId) { return new PocketBaseState( - ownerId, 0, Optional.empty(), Optional.empty(), false + ownerId, 0, Optional.empty(), Optional.empty(), false, + PocketBaseBiome.THE_VOID ); } public PocketBaseState withLevel(int newLevel) { return new PocketBaseState( - ownerId, newLevel, entrance, returnPortal, mobSpawningEnabled + ownerId, newLevel, entrance, returnPortal, mobSpawningEnabled, biome ); } public PocketBaseState withEntrance(PocketPortalLocation portal) { return new PocketBaseState( - ownerId, level, Optional.of(portal), returnPortal, mobSpawningEnabled + ownerId, level, Optional.of(portal), returnPortal, mobSpawningEnabled, biome ); } public PocketBaseState withoutEntrance() { return new PocketBaseState( - ownerId, level, Optional.empty(), returnPortal, mobSpawningEnabled + ownerId, level, Optional.empty(), returnPortal, mobSpawningEnabled, biome ); } public PocketBaseState withReturnPortal(PocketPortalLocation portal) { return new PocketBaseState( - ownerId, level, entrance, Optional.of(portal), mobSpawningEnabled + ownerId, level, entrance, Optional.of(portal), mobSpawningEnabled, biome ); } public PocketBaseState withMobSpawningEnabled(boolean enabled) { - return new PocketBaseState(ownerId, level, entrance, returnPortal, enabled); + return new PocketBaseState(ownerId, level, entrance, returnPortal, enabled, biome); + } + + public PocketBaseState withBiome(PocketBaseBiome selectedBiome) { + return new PocketBaseState( + ownerId, level, entrance, returnPortal, mobSpawningEnabled, selectedBiome + ); } } diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java b/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java index 0433c43..b294efe 100644 --- a/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java +++ b/src/main/java/games/dmg/spigotbase/PocketBaseWorldService.java @@ -4,11 +4,15 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.function.Function; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.Server; import org.bukkit.World; import org.bukkit.WorldCreator; +import org.bukkit.block.Biome; final class PocketBaseWorldService { private static final int PLATFORM_Y = 64; @@ -16,23 +20,43 @@ final class PocketBaseWorldService { private final Server server; private final PluginSettingsProvider settings; private final PocketBasePolicy policy; + private final Function biomeResolver; private final Map ownersByWorld = new HashMap<>(); PocketBaseWorldService(Server server, PluginSettingsProvider settings) { + this(server, settings, biome -> resolveBiome(server, biome)); + } + + PocketBaseWorldService( + Server server, + PluginSettingsProvider settings, + Function biomeResolver + ) { this.server = server; this.settings = settings; this.policy = new PocketBasePolicy(settings); + this.biomeResolver = biomeResolver; } void loadExisting(Map states) { for (PocketBaseState state : states.values()) { if (state.level() > 0) { + setBiome(state.ownerId(), state.level(), state.biome()); setMobSpawning(state.ownerId(), state.mobSpawningEnabled()); } } } World expand(UUID ownerId, int previousLevel, int newLevel) { + return expand(ownerId, previousLevel, newLevel, PocketBaseBiome.THE_VOID); + } + + World expand( + UUID ownerId, + int previousLevel, + int newLevel, + PocketBaseBiome biome + ) { World world = ensureWorld(ownerId); int newHalfSize = policy.size(newLevel) / 2; int previousHalfSize = previousLevel == 0 ? 0 : policy.size(previousLevel) / 2; @@ -50,6 +74,7 @@ final class PocketBaseWorldService { world.getBlockAt(x, PLATFORM_Y, z).setType(Material.GRASS_BLOCK, false); } } + applyBiome(world, previousLevel, newLevel, biome); if (previousLevel == 0) { createReturnPortal(world); world.setSpawnLocation(0, PLATFORM_Y + 1, 5); @@ -70,6 +95,12 @@ final class PocketBaseWorldService { ensureWorld(ownerId).setSpawnFlags(enabled, enabled); } + void setBiome(UUID ownerId, int level, PocketBaseBiome biome) { + World world = ensureWorld(ownerId); + applyBiome(world, 0, level, biome); + world.save(); + } + Location arrival(UUID ownerId) { return new Location(ensureWorld(ownerId), 0.5, PLATFORM_Y + 1, 5.5, 180.0f, 0.0f); } @@ -114,6 +145,40 @@ final class PocketBaseWorldService { return world; } + private void applyBiome( + World world, + int previousLevel, + int newLevel, + PocketBaseBiome selectedBiome + ) { + int newHalfSize = policy.size(newLevel) / 2; + int previousHalfSize = previousLevel == 0 ? 0 : policy.size(previousLevel) / 2; + Biome biome = biomeResolver.apply(selectedBiome); + for (int x = -newHalfSize; x < newHalfSize; x += 4) { + for (int z = -newHalfSize; z < newHalfSize; z += 4) { + boolean previouslyUnlocked = previousLevel > 0 + && x >= -previousHalfSize && x < previousHalfSize + && z >= -previousHalfSize && z < previousHalfSize; + if (previouslyUnlocked) { + continue; + } + for (int y = world.getMinHeight(); y < world.getMaxHeight(); y += 4) { + world.setBiome(x, y, z, biome); + } + } + } + } + + private static Biome resolveBiome(Server server, PocketBaseBiome selectedBiome) { + Registry biomeRegistry = server.getRegistry(Biome.class); + if (biomeRegistry == null) { + throw new IllegalStateException("Biome registry is unavailable"); + } + return biomeRegistry.getOrThrow( + NamespacedKey.minecraft(selectedBiome.commandName()) + ); + } + private void createReturnPortal(World world) { PocketPortalLocation portal = new PocketPortalLocation( world.getUID(), world.getName(), -2, PLATFORM_Y + 1, 0, PocketPortalAxis.X diff --git a/src/main/java/games/dmg/spigotbase/PocketBaseWorldType.java b/src/main/java/games/dmg/spigotbase/PocketBaseWorldType.java new file mode 100644 index 0000000..2036547 --- /dev/null +++ b/src/main/java/games/dmg/spigotbase/PocketBaseWorldType.java @@ -0,0 +1,22 @@ +package games.dmg.spigotbase; + +import java.util.Locale; +import java.util.Optional; + +public enum PocketBaseWorldType { + VOID, + NETHER, + OVERWORLD; + + public String commandName() { + return name().toLowerCase(Locale.ROOT); + } + + static Optional fromCommand(String value) { + try { + return Optional.of(valueOf(value.toUpperCase(Locale.ROOT))); + } catch (IllegalArgumentException exception) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java b/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java index cfcd3e4..e805ffe 100644 --- a/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java +++ b/src/main/java/games/dmg/spigotbase/YamlPocketBaseRepository.java @@ -44,7 +44,8 @@ public final class YamlPocketBaseRepository { level, loadPortal(yaml, path + ".entrance"), loadPortal(yaml, path + ".return-portal"), - yaml.getBoolean(path + ".mob-spawning-enabled", false) + yaml.getBoolean(path + ".mob-spawning-enabled", false), + loadBiome(yaml, path) ); states.put(ownerId, state); } catch (IllegalArgumentException ignored) { @@ -64,6 +65,8 @@ public final class YamlPocketBaseRepository { String path = "owners." + state.ownerId(); yaml.set(path + ".level", state.level()); yaml.set(path + ".mob-spawning-enabled", state.mobSpawningEnabled()); + 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) @@ -87,6 +90,17 @@ public final class YamlPocketBaseRepository { } } + private static PocketBaseBiome loadBiome(YamlConfiguration yaml, String path) { + String worldTypeName = yaml.getString(path + ".world-type"); + String biomeName = yaml.getString(path + ".biome"); + if (worldTypeName == null || biomeName == null) { + return PocketBaseBiome.THE_VOID; + } + return PocketBaseWorldType.fromCommand(worldTypeName) + .flatMap(worldType -> PocketBaseBiome.fromCommand(worldType, biomeName)) + .orElse(PocketBaseBiome.THE_VOID); + } + private static Optional loadPortal( YamlConfiguration yaml, String path diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3c4396c..8c93598 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -50,6 +50,8 @@ visitor-currency-material: DIAMOND pocket-base-unlock-cost: 64 pocket-base-upgrade-cost: 16 pocket-base-currency-material: DIAMOND_BLOCK +pocket-base-biome-change-cost: 16 +pocket-base-biome-currency-material: NETHERITE_BLOCK pocket-base-portal-frame-material: DIAMOND_BLOCK # Spawnable block overlay diff --git a/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java b/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java index d82a44a..e710ec1 100644 --- a/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java +++ b/src/test/java/games/dmg/spigotbase/BaseSettingsCommandTest.java @@ -100,6 +100,25 @@ final class BaseSettingsCommandTest { List.of("mobs"), command.onTabComplete(null, null, "basesettings", new String[] {"pocket", "m"}) ); + assertEquals( + List.of("type"), + command.onTabComplete(null, null, "basesettings", new String[] {"pocket", "t"}) + ); + assertEquals( + List.of("nether"), + command.onTabComplete( + null, null, "basesettings", new String[] {"pocket", "type", "n"} + ) + ); + assertEquals( + List.of("crimson_forest"), + command.onTabComplete( + null, + null, + "basesettings", + new String[] {"pocket", "type", "nether", "c"} + ) + ); assertEquals( List.of("enable"), command.onTabComplete( @@ -209,6 +228,73 @@ final class BaseSettingsCommandTest { verify(inventory).setStorageContents(any(ItemStack[].class)); } + @Test + void pocketOwnerCanPurchaseBiomeChangeWithConfiguredCurrency() throws Exception { + UUID playerId = UUID.randomUUID(); + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(playerId); + when(player.getName()).thenReturn("Builder"); + PlayerInventory inventory = mock(PlayerInventory.class); + when(player.getInventory()).thenReturn(inventory); + when(inventory.getStorageContents()).thenReturn(new ItemStack[] { + new ItemStack(Material.NETHERITE_BLOCK, 16) + }); + BaseStateManager stateManager = mock(BaseStateManager.class); + when(stateManager.player(playerId, "Builder")) + .thenReturn(PlayerState.newPlayer(playerId, "Builder")); + PocketBaseManager pocketBases = mock(PocketBaseManager.class); + PocketBaseState current = new PocketBaseState(playerId, 1, java.util.Optional.empty()); + when(pocketBases.state(playerId)).thenReturn(current); + when(pocketBases.setBiome(playerId, PocketBaseBiome.CRIMSON_FOREST)) + .thenReturn(current.withBiome(PocketBaseBiome.CRIMSON_FOREST)); + BaseSettingsCommand command = new BaseSettingsCommand( + stateManager, + new PluginSettingsProvider(PluginSettings.from(Map.of())), + mock(BaseFlightController.class), + pocketBases + ); + + command.onCommand(player, null, "basesettings", new String[] { + "pocket", "type", "nether", "crimson_forest" + }); + + verify(pocketBases).setBiome(playerId, PocketBaseBiome.CRIMSON_FOREST); + verify(inventory).setStorageContents(any(ItemStack[].class)); + } + + @Test + void selectingCurrentPocketBiomeDoesNotCharge() throws Exception { + UUID playerId = UUID.randomUUID(); + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(playerId); + when(player.getName()).thenReturn("Builder"); + PlayerInventory inventory = mock(PlayerInventory.class); + when(player.getInventory()).thenReturn(inventory); + BaseStateManager stateManager = mock(BaseStateManager.class); + when(stateManager.player(playerId, "Builder")) + .thenReturn(PlayerState.newPlayer(playerId, "Builder")); + PocketBaseManager pocketBases = mock(PocketBaseManager.class); + when(pocketBases.state(playerId)).thenReturn( + new PocketBaseState(playerId, 1, java.util.Optional.empty()) + ); + BaseSettingsCommand command = new BaseSettingsCommand( + stateManager, + new PluginSettingsProvider(PluginSettings.from(Map.of())), + mock(BaseFlightController.class), + pocketBases + ); + + command.onCommand(player, null, "basesettings", new String[] { + "pocket", "type", "void", "the_void" + }); + + verify(pocketBases, never()).setBiome( + org.mockito.ArgumentMatchers.eq(playerId), + org.mockito.ArgumentMatchers.any(PocketBaseBiome.class) + ); + verify(inventory, never()).setStorageContents(any(ItemStack[].class)); + } + @Test void pocketOwnerCanEnableNaturalMobSpawning() throws Exception { UUID playerId = UUID.randomUUID(); @@ -366,6 +452,7 @@ final class BaseSettingsCommandTest { verify(player).sendMessage(org.mockito.ArgumentMatchers.argThat( message -> message.contains("Pocket Base 1") + && message.contains("type=void/the_void") && message.contains("mob spawning=disabled") )); } diff --git a/src/test/java/games/dmg/spigotbase/PluginSettingsTest.java b/src/test/java/games/dmg/spigotbase/PluginSettingsTest.java index f0a234e..f3a2b97 100644 --- a/src/test/java/games/dmg/spigotbase/PluginSettingsTest.java +++ b/src/test/java/games/dmg/spigotbase/PluginSettingsTest.java @@ -25,6 +25,8 @@ final class PluginSettingsTest { assertEquals(16, settings.pocketBaseUpgradeCost()); assertEquals("DIAMOND_BLOCK", settings.pocketBaseCurrencyMaterial()); assertEquals("DIAMOND_BLOCK", settings.pocketBasePortalFrameMaterial()); + assertEquals(16, settings.pocketBaseBiomeChangeCost()); + assertEquals("NETHERITE_BLOCK", settings.pocketBaseBiomeCurrencyMaterial()); } @Test @@ -33,13 +35,17 @@ final class PluginSettingsTest { "pocket-base-unlock-cost", 32, "pocket-base-upgrade-cost", 8, "pocket-base-currency-material", "EMERALD_BLOCK", - "pocket-base-portal-frame-material", "GOLD_BLOCK" + "pocket-base-portal-frame-material", "GOLD_BLOCK", + "pocket-base-biome-change-cost", 12, + "pocket-base-biome-currency-material", "NETHERITE_INGOT" )); assertEquals(32, settings.pocketBaseUnlockCost()); assertEquals(8, settings.pocketBaseUpgradeCost()); assertEquals("EMERALD_BLOCK", settings.pocketBaseCurrencyMaterial()); assertEquals("GOLD_BLOCK", settings.pocketBasePortalFrameMaterial()); + assertEquals(12, settings.pocketBaseBiomeChangeCost()); + assertEquals("NETHERITE_INGOT", settings.pocketBaseBiomeCurrencyMaterial()); } @Test diff --git a/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java b/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java index 8bb3be5..b62f71f 100644 --- a/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java +++ b/src/test/java/games/dmg/spigotbase/PocketBaseManagerTest.java @@ -85,6 +85,57 @@ final class PocketBaseManagerTest { ); } + @Test + void persistsBiomeAfterApplyingItToTheUnlockedArea() throws Exception { + UUID ownerId = UUID.randomUUID(); + PocketBaseState current = new PocketBaseState(ownerId, 2, Optional.empty()); + PocketBaseStateManager states = mock(PocketBaseStateManager.class); + PocketBaseWorldService worlds = mock(PocketBaseWorldService.class); + when(states.state(ownerId)).thenReturn(current); + when(states.updateAndSave(org.mockito.ArgumentMatchers.eq(ownerId), + org.mockito.ArgumentMatchers.any())).thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + UnaryOperator operation = invocation.getArgument(1); + return operation.apply(current); + }); + PocketBaseManager manager = new PocketBaseManager( + states, + worlds, + new PluginSettingsProvider(PluginSettings.from(Map.of())) + ); + + PocketBaseState updated = manager.setBiome(ownerId, PocketBaseBiome.WARPED_FOREST); + + assertEquals(PocketBaseBiome.WARPED_FOREST, updated.biome()); + InOrder order = inOrder(worlds, states); + order.verify(worlds).setBiome(ownerId, 2, PocketBaseBiome.WARPED_FOREST); + order.verify(states).updateAndSave( + org.mockito.ArgumentMatchers.eq(ownerId), org.mockito.ArgumentMatchers.any() + ); + } + + @Test + void restoresPreviousBiomeWhenPersistenceFails() throws Exception { + UUID ownerId = UUID.randomUUID(); + PocketBaseState current = new PocketBaseState(ownerId, 1, Optional.empty()); + PocketBaseStateManager states = mock(PocketBaseStateManager.class); + PocketBaseWorldService worlds = mock(PocketBaseWorldService.class); + when(states.state(ownerId)).thenReturn(current); + when(states.updateAndSave(org.mockito.ArgumentMatchers.eq(ownerId), + org.mockito.ArgumentMatchers.any())).thenThrow(new IOException("save failed")); + PocketBaseManager manager = new PocketBaseManager( + states, + worlds, + new PluginSettingsProvider(PluginSettings.from(Map.of())) + ); + + assertThrows(IOException.class, + () -> manager.setBiome(ownerId, PocketBaseBiome.DESERT)); + + verify(worlds).setBiome(ownerId, 1, PocketBaseBiome.DESERT); + verify(worlds).setBiome(ownerId, 1, PocketBaseBiome.THE_VOID); + } + @Test void restoresWorldSettingWhenPersistenceFails() throws Exception { UUID ownerId = UUID.randomUUID(); diff --git a/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java b/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java index ee2768f..9122f74 100644 --- a/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java +++ b/src/test/java/games/dmg/spigotbase/PocketBaseWorldServiceTest.java @@ -31,9 +31,12 @@ final class PocketBaseWorldServiceTest { when(server.createWorld(any(WorldCreator.class))).thenReturn(world); when(world.getUID()).thenReturn(worldId); when(world.getName()).thenReturn("pocket"); + when(world.getMinHeight()).thenReturn(0); + when(world.getMaxHeight()).thenReturn(8); PocketBaseWorldService service = new PocketBaseWorldService( server, - new PluginSettingsProvider(PluginSettings.from(Map.of())) + new PluginSettingsProvider(PluginSettings.from(Map.of())), + ignored -> null ); service.expand(ownerId, 0, 1); @@ -84,7 +87,8 @@ final class PocketBaseWorldServiceTest { when(world.getUID()).thenReturn(UUID.randomUUID()); PocketBaseWorldService service = new PocketBaseWorldService( server, - new PluginSettingsProvider(PluginSettings.from(Map.of())) + new PluginSettingsProvider(PluginSettings.from(Map.of())), + ignored -> null ); service.loadExisting(Map.of( @@ -95,6 +99,32 @@ final class PocketBaseWorldServiceTest { verify(world).setSpawnFlags(true, true); } + @Test + void appliesSelectedBiomeAcrossUnlockedColumnsWithoutChangingBlocks() { + UUID ownerId = UUID.randomUUID(); + Server server = mock(Server.class); + World world = mock(World.class); + when(server.getWorld(org.mockito.ArgumentMatchers.anyString())).thenReturn(world); + when(world.getUID()).thenReturn(UUID.randomUUID()); + when(world.getMinHeight()).thenReturn(0); + when(world.getMaxHeight()).thenReturn(8); + PocketBaseWorldService service = new PocketBaseWorldService( + server, + new PluginSettingsProvider(PluginSettings.from(Map.of())), + ignored -> null + ); + + service.setBiome(ownerId, 1, PocketBaseBiome.CRIMSON_FOREST); + + verify(world).setBiome(-32, 0, -32, null); + verify(world).setBiome(28, 4, 28, null); + verify(world, never()).getBlockAt( + org.mockito.ArgumentMatchers.anyInt(), + org.mockito.ArgumentMatchers.anyInt(), + org.mockito.ArgumentMatchers.anyInt() + ); + } + @Test void laterLevelAddsRingWithoutOverwritingExistingTerrain() { UUID ownerId = UUID.randomUUID(); @@ -104,9 +134,12 @@ final class PocketBaseWorldServiceTest { when(server.createWorld(any(WorldCreator.class))).thenReturn(world); when(world.getUID()).thenReturn(UUID.randomUUID()); when(world.getName()).thenReturn("pocket"); + when(world.getMinHeight()).thenReturn(0); + when(world.getMaxHeight()).thenReturn(8); PocketBaseWorldService service = new PocketBaseWorldService( server, - new PluginSettingsProvider(PluginSettings.from(Map.of())) + new PluginSettingsProvider(PluginSettings.from(Map.of())), + ignored -> null ); Block existingCenter = world.getBlockAt(0, 64, 0); @@ -116,6 +149,8 @@ final class PocketBaseWorldServiceTest { .setType(Material.GRASS_BLOCK, false); verify(existingCenter, never()) .setType(any(Material.class), org.mockito.ArgumentMatchers.anyBoolean()); + verify(world).setBiome(44, 4, 44, null); + verify(world, never()).setBiome(0, 0, 0, null); } private static Map blocks(World world) { diff --git a/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java b/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java index 470240d..d56030f 100644 --- a/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java +++ b/src/test/java/games/dmg/spigotbase/YamlPocketBaseRepositoryTest.java @@ -29,7 +29,8 @@ final class YamlPocketBaseRepositoryTest { Optional.of(new PocketPortalLocation( worldId, "pocket", 20, 65, 12, PocketPortalAxis.Z )), - true + true, + PocketBaseBiome.CRIMSON_FOREST ); YamlPocketBaseRepository repository = new YamlPocketBaseRepository( temporaryDirectory.resolve("pocket-bases.yml") @@ -51,5 +52,6 @@ final class YamlPocketBaseRepositoryTest { assertFalse(loaded.mobSpawningEnabled()); assertTrue(loaded.returnPortal().isEmpty()); + assertEquals(PocketBaseBiome.THE_VOID, loaded.biome()); } }