feat(settings): add native base control dialog
Release / release (push) Successful in 2m45s
CI / build (push) Successful in 1m18s

This commit is contained in:
dmg
2026-08-24 10:54:26 -04:00
parent b2ba4d5821
commit b6bbf9183a
10 changed files with 589 additions and 12 deletions
+3
View File
@@ -26,6 +26,7 @@ The plugin JAR is written to `build/libs/`.
/base (alias: /home) /base (alias: /home)
/gotobase <player> (alias: /visit <player>) /gotobase <player> (alias: /visit <player>)
/basesettings (alias: /homesettings) /basesettings (alias: /homesettings)
/basesettings ui
/basesettings status /basesettings status
/basesettings upgrade /basesettings upgrade
/basesettings pocket upgrade /basesettings pocket upgrade
@@ -39,6 +40,8 @@ The plugin JAR is written to `build/libs/`.
/basesettings bossbar <enable|disable> /basesettings bossbar <enable|disable>
``` ```
`/basesettings` and `/basesettings ui` open Minecraft's native dialog dashboard. The dashboard summarizes Base and Pocket Base progression, provides unlocked setting controls and purchase confirmations, and marks unavailable controls as locked. `/basesettings status` retains the detailed chat report.
Navigation particles appear only in the base's world and when the player is more than 25 blocks beyond the current base border. Navigation particles appear only in the base's world and when the player is more than 25 blocks beyond the current base border.
After 250 Survival-mode block placements anywhere by default, the spawnable overlay can mark nearby dark hostile-mob spawning surfaces inside the player's base with owner-only red particles. The threshold is configurable. After 250 Survival-mode block placements anywhere by default, the spawnable overlay can mark nearby dark hostile-mob spawning surfaces inside the player's base with owner-only red particles. The threshold is configurable.
+8
View File
@@ -114,3 +114,11 @@ description: Chronological record of material decisions affecting the Spigot Bas
- Applied biome metadata across unlocked columns and new expansion rings without changing blocks, entities, inventories, portals, progression, or mob-spawning preferences. - 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. - 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`. - Verified the feature with `./gradlew clean check jar`.
## 2026-08-24 — Native base settings dialog
- Changed `/basesettings` and `/homesettings` with no arguments to open a native Minecraft dialog dashboard, with explicit `ui` subcommands providing the same behavior.
- Added progression summaries, current and locked setting states, toggles, Pocket Base controls, biome selection, and confirmations for Base IV, Pocket Base, and biome purchases.
- Retained the detailed chat report under `/basesettings status` and kept dialog actions routed through the existing validated command behavior.
- Added dialog routing, autocomplete, alias, non-player, generated-control, locked-state, and purchase-confirmation coverage.
- Verified the feature with `./gradlew clean check jar`.
+1
View File
@@ -25,3 +25,4 @@ description: Catalog of user stories for the Spigot Base plugin.
17. [US-017: Expand a Pocket Base](us-017-expand-a-pocket-base.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) 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) 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)
@@ -11,7 +11,7 @@ As a **player**, I want to inspect my progression and receive timely milestone f
## Acceptance criteria ## Acceptance criteria
- [x] `/basesettings` and `/basesettings status` show 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 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] Each path identifies earned levels, unmet prerequisites, current progress, the next threshold, and the next reward. - [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] 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. - [x] Relevant qualifying activity briefly displays a configurable progress boss bar for the active milestone.
@@ -19,7 +19,7 @@ As a **player**, I want to inspect my progression and receive timely milestone f
- [x] The automatic boss bar disappears after a configurable number of seconds. - [x] The automatic boss bar disappears after a configurable number of seconds.
- [x] `/basesettings bossbar enable` enables automatic progress boss bars idempotently, and `/basesettings bossbar disable` disables them idempotently. - [x] `/basesettings bossbar enable` enables automatic progress boss bars idempotently, and `/basesettings bossbar disable` disables them idempotently.
- [x] Invalid boss-bar arguments show command usage, and `enable` and `disable` are offered through autocomplete. - [x] Invalid boss-bar arguments show command usage, and `enable` and `disable` are offered through autocomplete.
- [x] Disabling automatic boss bars does not prevent `/basesettings` from displaying progress. - [x] Disabling automatic boss bars does not prevent `/basesettings status` from displaying progress.
- [x] The boss-bar preference persists across reconnects and restarts. - [x] The boss-bar preference persists across reconnects and restarts.
- [x] Each newly unlocked level displays prominent full-screen title and subtitle text describing the reward. - [x] Each newly unlocked level displays prominent full-screen title and subtitle text describing the reward.
- [x] Unlock notifications occur once per earned level and do not repeat after reconnecting or restarting. - [x] Unlock notifications occur once per earned level and do not repeat after reconnecting or restarting.
@@ -0,0 +1,31 @@
---
type: User Story
title: "US-020: Control a base through a dialog UI"
description: Let players inspect and control their base through Minecraft's native dialog interface.
status: done
---
# US-020: Control a base through a dialog UI
As a **player**, I want a dialog-based base dashboard so that I can inspect progression and manage my base without remembering command arguments.
## Acceptance criteria
- [x] `/basesettings` and `/basesettings ui` open the base-control dialog.
- [x] `/homesettings` and `/homesettings ui` behave identically.
- [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] 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.
- [x] Non-player execution remains safely rejected.
- [x] Automated tests verify dialog routing and generated controls.
## Related
- [US-008: Unlock Base IV visitor access](us-008-unlock-visitor-access.md)
- [US-009: View progression and unlock notifications](us-009-view-progression-and-notifications.md)
- [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)
@@ -15,7 +15,7 @@ import org.bukkit.inventory.PlayerInventory;
final class BaseSettingsCommand implements CommandExecutor, TabCompleter { final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
private static final List<String> SETTINGS = List.of( private static final List<String> SETTINGS = List.of(
"status", "upgrade", "pocket", "visitors", "navigation", "flight", "border", "ui", "status", "upgrade", "pocket", "visitors", "navigation", "flight", "border",
"spawnable", "bossbar" "spawnable", "bossbar"
); );
private static final List<String> VISITOR_MODES = List.of("allowed", "blocked"); private static final List<String> VISITOR_MODES = List.of("allowed", "blocked");
@@ -58,7 +58,11 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
} }
PlayerState state = stateManager.player(player.getUniqueId(), player.getName()); PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
if (arguments.length == 0 if (arguments.length == 0
|| arguments.length == 1 && arguments[0].equalsIgnoreCase("status")) { || arguments.length == 1 && arguments[0].equalsIgnoreCase("ui")) {
showDialog(player, state);
return true;
}
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("status")) {
showStatus(player, state); showStatus(player, state);
return true; return true;
} }
@@ -441,6 +445,13 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
return true; return true;
} }
private void showDialog(Player player, PlayerState state) {
PocketBaseState pocket = pocketBases == null
? PocketBaseState.locked(state.playerId())
: pocketBases.state(state.playerId());
player.showDialog(new BaseSettingsDialogFactory(settings.current()).create(state, pocket));
}
private void showStatus(Player player, PlayerState state) { private void showStatus(Player player, PlayerState state) {
player.sendMessage(ChatColor.GOLD + "=== Base Progress ==="); player.sendMessage(ChatColor.GOLD + "=== Base Progress ===");
showBasePath(player, state); showBasePath(player, state);
@@ -663,7 +674,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
private static void sendUsage(Player player) { private static void sendUsage(Player player) {
player.sendMessage(ChatColor.RED + "Usage: /basesettings " player.sendMessage(ChatColor.RED + "Usage: /basesettings "
+ "[status|upgrade|pocket upgrade|pocket mobs " + "[ui|status|upgrade|pocket upgrade|pocket mobs "
+ "<hostile|passive> <enable|disable>" + "<hostile|passive> <enable|disable>"
+ "|pocket type <void|nether|overworld> <subtype>" + "|pocket type <void|nether|overworld> <subtype>"
+ "|visitors <allowed|blocked>|navigation <enable|disable>" + "|visitors <allowed|blocked>|navigation <enable|disable>"
@@ -0,0 +1,353 @@
package games.dmg.spigotbase;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.dialog.ConfirmationDialog;
import net.md_5.bungee.api.dialog.Dialog;
import net.md_5.bungee.api.dialog.DialogBase;
import net.md_5.bungee.api.dialog.DialogListDialog;
import net.md_5.bungee.api.dialog.MultiActionDialog;
import net.md_5.bungee.api.dialog.NoticeDialog;
import net.md_5.bungee.api.dialog.action.ActionButton;
import net.md_5.bungee.api.dialog.action.RunCommandAction;
import net.md_5.bungee.api.dialog.body.PlainMessageBody;
final class BaseSettingsDialogFactory {
private static final int DIALOG_WIDTH = 420;
private final PluginSettings settings;
BaseSettingsDialogFactory(PluginSettings settings) {
this.settings = settings;
}
Dialog create(PlayerState owner, PocketBaseState pocket) {
return new DialogListDialog(
base(
"Base Settings",
"Base Settings",
"Base " + owner.baseLevel() + "/4 • Size " + owner.sizeLevel() + "/3"
+ " • Flight " + owner.flightLevel() + "/3\n"
+ "Pocket Base " + pocket.level() + ""
+ humanize(pocket.biome().commandName())
),
List.of(
generalSettings(owner),
progressReport(),
baseUpgrade(owner),
pocketSettings(owner, pocket)
),
null,
2,
190
);
}
private Dialog generalSettings(PlayerState owner) {
return new MultiActionDialog(
base(
"Base Controls",
"Base Controls",
"Select a setting to change it. Locked controls remain visible and explain "
+ "their requirement in chat when selected."
),
List.of(
toggle(
"Visitors",
owner.baseLevel() >= 4,
owner.visitorsEnabled(),
"Allowed",
"Blocked",
"basesettings visitors "
+ (owner.visitorsEnabled() ? "blocked" : "allowed")
),
toggle(
"Navigation",
owner.baseLevel() >= 2,
owner.navigationEnabled(),
"Enabled",
"Disabled",
"basesettings navigation "
+ mode(!owner.navigationEnabled())
),
toggle(
"Flight",
owner.flightLevel() >= 1,
owner.flightEnabled(),
"Enabled",
"Disabled",
"basesettings flight " + mode(!owner.flightEnabled())
),
toggle(
"Border",
owner.baseLevel() >= 1 && owner.base().isPresent(),
owner.borderEnabled(),
"Enabled",
"Disabled",
"basesettings border " + mode(!owner.borderEnabled())
),
toggle(
"Spawnable Overlay",
owner.baseLevel() >= 1
&& owner.base().isPresent()
&& owner.totalBlocksPlaced()
>= settings.spawnableOverlayUnlockPlacements(),
owner.spawnableOverlayEnabled(),
"Enabled",
"Disabled",
"basesettings spawnable " + mode(!owner.spawnableOverlayEnabled())
),
toggle(
"Progress Boss Bar",
true,
owner.bossBarEnabled(),
"Enabled",
"Disabled",
"basesettings bossbar " + mode(!owner.bossBarEnabled())
)
),
2,
null
);
}
private Dialog progressReport() {
return new NoticeDialog(base(
"Progress Report",
"Progress Report",
"Display the detailed progression report in chat."
)).action(commandButton("Show in Chat", "basesettings status"));
}
private Dialog baseUpgrade(PlayerState owner) {
if (owner.baseLevel() >= 4) {
return notice("Base IV", "Base IV and visitor access are already unlocked.");
}
if (owner.baseLevel() < 3 || owner.base().isEmpty()) {
return notice(
"Base IV — Locked",
"Unlock and establish Base III before purchasing Base IV visitor access."
);
}
int price = settings.visitorUnlockDiamondCost();
return confirmation(
"Unlock Base IV",
"Purchase visitor access for " + price + " diamonds?",
"basesettings upgrade"
);
}
private Dialog pocketSettings(PlayerState owner, PocketBaseState pocket) {
return new DialogListDialog(
base(
"Pocket Base",
"Pocket Base " + pocket.level(),
pocket.level() == 0
? "Pocket Base is locked."
: "Biome: " + pocket.biome().worldType().commandName() + "/"
+ pocket.biome().commandName() + "\nHostile mobs: "
+ enabled(pocket.hostileMobSpawningEnabled()) + " • Passive mobs: "
+ enabled(pocket.passiveMobSpawningEnabled())
),
List.of(
pocketUpgrade(owner, pocket),
pocketMobSettings(pocket),
pocketBiomeSettings(pocket)
),
null,
1,
260
);
}
private Dialog pocketUpgrade(PlayerState owner, PocketBaseState pocket) {
if (owner.baseLevel() < 4 || owner.base().isEmpty()) {
return notice(
"Pocket Upgrade — Locked",
"Establish Base IV before purchasing a Pocket Base."
);
}
int price = pocket.level() == 0
? settings.pocketBaseUnlockCost()
: settings.pocketBaseUpgradeCost();
String currency = humanize(settings.pocketBaseCurrencyMaterial());
String title = pocket.level() == 0
? "Unlock Pocket Base I"
: "Upgrade to Pocket Base " + (pocket.level() + 1);
return confirmation(
title,
"Purchase this upgrade for " + price + " " + currency + "?",
"basesettings pocket upgrade"
);
}
private Dialog pocketMobSettings(PocketBaseState pocket) {
if (pocket.level() < 1) {
return notice(
"Mob Spawning — Locked",
"Unlock Pocket Base I to control natural mob spawning."
);
}
return new MultiActionDialog(
base(
"Mob Spawning",
"Mob Spawning",
"Control natural spawning independently for each mob category."
),
List.of(
toggle(
"Hostile Mobs",
true,
pocket.hostileMobSpawningEnabled(),
"Enabled",
"Disabled",
"basesettings pocket mobs hostile "
+ mode(!pocket.hostileMobSpawningEnabled())
),
toggle(
"Passive Mobs",
true,
pocket.passiveMobSpawningEnabled(),
"Enabled",
"Disabled",
"basesettings pocket mobs passive "
+ mode(!pocket.passiveMobSpawningEnabled())
)
),
1,
null
);
}
private Dialog pocketBiomeSettings(PocketBaseState pocket) {
if (pocket.level() < 1) {
return notice(
"Change Biome — Locked",
"Unlock Pocket Base I to purchase biome changes."
);
}
List<Dialog> worldTypes = Arrays.stream(PocketBaseWorldType.values())
.map(type -> pocketBiomeType(pocket, type))
.map(Dialog.class::cast)
.toList();
return new DialogListDialog(
base(
"Change Biome",
"Change Biome",
"Current biome: " + pocket.biome().worldType().commandName() + "/"
+ pocket.biome().commandName() + "\nEach change costs "
+ settings.pocketBaseBiomeChangeCost() + " "
+ humanize(settings.pocketBaseBiomeCurrencyMaterial()) + "."
),
worldTypes,
null,
1,
220
);
}
private DialogListDialog pocketBiomeType(
PocketBaseState pocket,
PocketBaseWorldType worldType
) {
List<Dialog> biomes = Arrays.stream(PocketBaseBiome.values())
.filter(biome -> biome.worldType() == worldType)
.map(biome -> biomeConfirmation(pocket, biome))
.map(Dialog.class::cast)
.toList();
return new DialogListDialog(
base(
humanize(worldType.commandName()),
humanize(worldType.commandName()) + " Biomes",
"Choose a biome subtype. The active biome is marked as selected."
),
biomes,
null,
2,
190
);
}
private Dialog biomeConfirmation(PocketBaseState pocket, PocketBaseBiome biome) {
String title = humanize(biome.commandName())
+ (pocket.biome() == biome ? " — Selected" : "");
if (pocket.biome() == biome) {
return notice(title, "This is your Pocket Base's current biome.");
}
return confirmation(
title,
"Change the Pocket Base biome for " + settings.pocketBaseBiomeChangeCost()
+ " " + humanize(settings.pocketBaseBiomeCurrencyMaterial()) + "?",
"basesettings pocket type " + biome.worldType().commandName() + " "
+ biome.commandName()
);
}
private static Dialog confirmation(String title, String message, String command) {
return new ConfirmationDialog(base(title, title, message))
.yes(commandButton("Confirm", command));
}
private static Dialog notice(String title, String message) {
return new NoticeDialog(base(title, title, message));
}
private static ActionButton toggle(
String name,
boolean unlocked,
boolean active,
String activeLabel,
String inactiveLabel,
String command
) {
String state = unlocked ? (active ? activeLabel : inactiveLabel) : "Locked";
String tooltip = unlocked
? "Click to change this setting."
: "This setting has not been unlocked yet.";
return new ActionButton(
text(name + ": " + state),
text(tooltip),
190,
new RunCommandAction("/" + command)
);
}
private static ActionButton commandButton(String label, String command) {
return new ActionButton(text(label), new RunCommandAction("/" + command));
}
private static DialogBase base(String title, String externalTitle, String message) {
return new DialogBase(text(title))
.externalTitle(text(externalTitle))
.body(List.of(new PlainMessageBody(text(message), DIALOG_WIDTH)))
.canCloseWithEscape(true)
.pause(false)
.afterAction(DialogBase.AfterAction.CLOSE);
}
private static BaseComponent text(String value) {
return new TextComponent(value);
}
private static String mode(boolean active) {
return active ? "enable" : "disable";
}
private static String enabled(boolean active) {
return active ? "enabled" : "disabled";
}
private static String humanize(String value) {
String normalized = value.toLowerCase(Locale.ROOT).replace('_', ' ');
StringBuilder result = new StringBuilder(normalized.length());
boolean capitalize = true;
for (int index = 0; index < normalized.length(); index++) {
char character = normalized.charAt(index);
result.append(capitalize ? Character.toUpperCase(character) : character);
capitalize = character == ' ';
}
return result.toString();
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ commands:
aliases: [visit] aliases: [visit]
basesettings: basesettings:
description: View progression, upgrade, and manage base settings. description: View progression, upgrade, and manage base settings.
usage: /basesettings [status|upgrade|pocket|visitors|navigation|flight|border|spawnable|bossbar] usage: /basesettings [ui|status|upgrade|pocket|visitors|navigation|flight|border|spawnable|bossbar]
aliases: [homesettings] aliases: [homesettings]
baseadmin: baseadmin:
description: Administer Spigot Base. description: Administer Spigot Base.
@@ -20,13 +20,29 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.PlayerInventory;
import net.md_5.bungee.api.dialog.Dialog;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
final class BaseSettingsCommandTest { final class BaseSettingsCommandTest {
@Test
void rejectsNonPlayerExecution() {
CommandSender sender = mock(CommandSender.class);
BaseSettingsCommand command = new BaseSettingsCommand(
mock(BaseStateManager.class),
new PluginSettingsProvider(PluginSettings.from(Map.of())),
mock(BaseFlightController.class)
);
command.onCommand(sender, null, "basesettings", new String[0]);
verify(sender).sendMessage("Only players have base progression.");
}
@Test @Test
void spawnableEnableIsIdempotentAfterUnlock() { void spawnableEnableIsIdempotentAfterUnlock() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
@@ -156,8 +172,8 @@ final class BaseSettingsCommandTest {
assertEquals( assertEquals(
List.of( List.of(
"status", "upgrade", "pocket", "visitors", "navigation", "flight", "border", "ui", "status", "upgrade", "pocket", "visitors", "navigation", "flight",
"spawnable", "bossbar" "border", "spawnable", "bossbar"
), ),
command.onTabComplete(null, null, "basesettings", new String[] {""}) command.onTabComplete(null, null, "basesettings", new String[] {""})
); );
@@ -526,7 +542,7 @@ final class BaseSettingsCommandTest {
} }
@Test @Test
void defaultAndStatusDisplayTheFullProgressReport() { void defaultAndUiOpenDialogWhileStatusDisplaysTheFullProgressReport() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
Player player = mock(Player.class); Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId); when(player.getUniqueId()).thenReturn(playerId);
@@ -542,17 +558,19 @@ final class BaseSettingsCommandTest {
); );
command.onCommand(player, null, "basesettings", new String[0]); command.onCommand(player, null, "basesettings", new String[0]);
command.onCommand(player, null, "homesettings", new String[] {"ui"});
command.onCommand(player, null, "basesettings", new String[] {"status"}); command.onCommand(player, null, "basesettings", new String[] {"status"});
verify(player, times(2)).sendMessage(ChatColor.GOLD + "=== Base Progress ==="); verify(player, times(2)).showDialog(any(Dialog.class));
verify(player, times(2)).sendMessage( verify(player).sendMessage(ChatColor.GOLD + "=== Base Progress ===");
verify(player).sendMessage(
org.mockito.ArgumentMatchers.<String>argThat(message -> org.mockito.ArgumentMatchers.<String>argThat(message ->
message.contains("Spawnable Overlay:") message.contains("Spawnable Overlay:")
&& message.contains("0/250 placements") && message.contains("0/250 placements")
&& message.contains("locked") && message.contains("locked")
) )
); );
verify(player, times(2)).sendMessage( verify(player).sendMessage(
org.mockito.ArgumentMatchers.<String>argThat(message -> org.mockito.ArgumentMatchers.<String>argThat(message ->
message.contains("Settings: visitors=") message.contains("Settings: visitors=")
&& message.contains("navigation=") && message.contains("navigation=")
@@ -0,0 +1,152 @@
package games.dmg.spigotbase;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import net.md_5.bungee.api.dialog.ConfirmationDialog;
import net.md_5.bungee.api.dialog.Dialog;
import net.md_5.bungee.api.dialog.DialogListDialog;
import net.md_5.bungee.api.dialog.MultiActionDialog;
import net.md_5.bungee.api.dialog.NoticeDialog;
import net.md_5.bungee.api.dialog.action.Action;
import net.md_5.bungee.api.dialog.action.ActionButton;
import net.md_5.bungee.api.dialog.action.RunCommandAction;
import org.junit.jupiter.api.Test;
final class BaseSettingsDialogFactoryTest {
@Test
void createsControlsForEveryBaseSettingAndConfirmedPurchase() {
UUID playerId = UUID.randomUUID();
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
.withAdministrativeLevels(4, 2, 2, 1, 1, true, true, true)
.withBase(
new BaseLocation(UUID.randomUUID(), "world", 10, 64, 20, 10, 20),
Instant.EPOCH
)
.withBorderEnabled(true)
.withTotalBlocksPlaced(250)
.withSpawnableOverlayEnabled(true);
PocketBaseState pocket = new PocketBaseState(
playerId,
1,
Optional.empty(),
Optional.empty(),
true,
false,
PocketBaseBiome.PLAINS
);
Dialog dialog = new BaseSettingsDialogFactory(
PluginSettings.from(Map.of())
).create(owner, pocket);
DialogListDialog root = assertInstanceOf(DialogListDialog.class, dialog);
assertTrue(root.getBase().body().get(0).toString().contains("Base 4/4"));
List<String> commands = commands(root);
assertTrue(commands.contains("/basesettings status"));
assertTrue(commands.contains("/basesettings visitors blocked"));
assertTrue(commands.contains("/basesettings navigation disable"));
assertTrue(commands.contains("/basesettings flight disable"));
assertTrue(commands.contains("/basesettings border disable"));
assertTrue(commands.contains("/basesettings spawnable disable"));
assertTrue(commands.contains("/basesettings bossbar disable"));
assertTrue(commands.contains("/basesettings pocket upgrade"));
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"));
}
@Test
void requiresConfirmationForBaseIvPurchase() {
UUID playerId = UUID.randomUUID();
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
.withAdministrativeLevels(3, 0, 0, 0, 0, false, false, false)
.withBase(
new BaseLocation(UUID.randomUUID(), "world", 0, 64, 0, 0, 0),
Instant.EPOCH
);
DialogListDialog root = assertInstanceOf(
DialogListDialog.class,
new BaseSettingsDialogFactory(PluginSettings.from(Map.of())).create(
owner,
PocketBaseState.locked(playerId)
)
);
ConfirmationDialog upgrade = assertInstanceOf(
ConfirmationDialog.class,
root.dialogs().get(2)
);
RunCommandAction action = assertInstanceOf(
RunCommandAction.class,
upgrade.yes().action()
);
assertTrue(action.template().equals("/basesettings upgrade"));
}
@Test
void marksUnavailableControlsAsLocked() {
UUID playerId = UUID.randomUUID();
PlayerState owner = PlayerState.newPlayer(playerId, "Newcomer");
DialogListDialog root = assertInstanceOf(
DialogListDialog.class,
new BaseSettingsDialogFactory(PluginSettings.from(Map.of())).create(
owner,
PocketBaseState.locked(playerId)
)
);
MultiActionDialog controls = assertInstanceOf(
MultiActionDialog.class,
root.dialogs().get(0)
);
List<String> labels = controls.actions().stream()
.map(ActionButton::label)
.map(Object::toString)
.toList();
assertTrue(labels.stream().anyMatch(label -> label.contains("Visitors: Locked")));
assertTrue(labels.stream().anyMatch(label -> label.contains("Navigation: Locked")));
assertTrue(labels.stream().anyMatch(label -> label.contains("Flight: Locked")));
assertTrue(labels.stream().anyMatch(label -> label.contains("Border: Locked")));
assertTrue(labels.stream().anyMatch(label -> label.contains("Spawnable Overlay: Locked")));
}
private static List<String> commands(Dialog dialog) {
List<String> commands = new ArrayList<>();
collect(dialog, commands);
return commands;
}
private static void collect(Dialog dialog, List<String> commands) {
if (dialog instanceof DialogListDialog list) {
list.dialogs().forEach(child -> collect(child, commands));
collect(list.exitAction(), commands);
} else if (dialog instanceof MultiActionDialog actions) {
actions.actions().forEach(button -> collect(button, commands));
collect(actions.exitAction(), commands);
} else if (dialog instanceof ConfirmationDialog confirmation) {
collect(confirmation.yes(), commands);
collect(confirmation.no(), commands);
} else if (dialog instanceof NoticeDialog notice) {
collect(notice.action(), commands);
}
}
private static void collect(ActionButton button, List<String> commands) {
if (button == null) {
return;
}
Action action = button.action();
if (action instanceof RunCommandAction command) {
commands.add(command.template());
}
}
}