diff --git a/design/log.md b/design/log.md index 78f27a5..92b7e90 100644 --- a/design/log.md +++ b/design/log.md @@ -28,6 +28,11 @@ description: Chronological record of material decisions affecting the Spigot Bas - Verified the implementation with `./gradlew clean check jar`: 34 tests passed and the plugin JAR was produced successfully. - User stories remain in progress pending live-server integration verification and completion of runtime administrative configuration editing. +## 2026-08-09 — Navigation preference fix + +- Added explicit, idempotent `/basenavigation on` and `/basenavigation off` modes with autocomplete while retaining no-argument toggling. +- Preserved a disabled navigation preference during later grass-or-dirt progression instead of forcing navigation back on. + ## 2026-08-09 — Initial release scope completed - Added live, validated, persisted numeric configuration updates through `/baseadmin config`. diff --git a/design/user-stories/us-002-unlock-base-navigation.md b/design/user-stories/us-002-unlock-base-navigation.md index 76d5113..e51a7dc 100644 --- a/design/user-stories/us-002-unlock-base-navigation.md +++ b/design/user-stories/us-002-unlock-base-navigation.md @@ -18,6 +18,9 @@ As a **player with Base I**, I want visual guidance toward my base so that I can - [x] Particle generation is bounded to avoid excessive server or client load. - [x] A player in another world receives a clear message instead of a misleading particle direction. - [x] `/basenavigation` toggles guidance on and off after Base II is unlocked. +- [x] `/basenavigation on` enables guidance idempotently and `/basenavigation off` disables it idempotently. +- [x] Invalid navigation arguments show command usage, and `on` and `off` are offered through autocomplete. +- [x] Grass-or-dirt progress after Base II preserves the player's selected navigation preference. - [x] The navigation preference persists across reconnects and restarts. ## Related diff --git a/src/main/java/games/dmg/spigotbase/BaseNavigationCommand.java b/src/main/java/games/dmg/spigotbase/BaseNavigationCommand.java index b10dc06..556a89c 100644 --- a/src/main/java/games/dmg/spigotbase/BaseNavigationCommand.java +++ b/src/main/java/games/dmg/spigotbase/BaseNavigationCommand.java @@ -1,12 +1,17 @@ package games.dmg.spigotbase; +import java.util.List; +import java.util.Locale; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; -final class BaseNavigationCommand implements CommandExecutor { +final class BaseNavigationCommand implements CommandExecutor, TabCompleter { + private static final List MODES = List.of("on", "off"); + private final BaseStateManager stateManager; BaseNavigationCommand(BaseStateManager stateManager) { @@ -28,10 +33,17 @@ final class BaseNavigationCommand implements CommandExecutor { player.sendMessage(ChatColor.RED + "Set your base before enabling navigation."); return true; } + final boolean enabled; + try { + enabled = NavigationPreference.resolve(state.navigationEnabled(), arguments); + } catch (IllegalArgumentException exception) { + player.sendMessage(ChatColor.RED + "Usage: /basenavigation [on|off]"); + return true; + } state = stateManager.update( player.getUniqueId(), player.getName(), - current -> current.withNavigationEnabled(!current.navigationEnabled()) + current -> current.withNavigationEnabled(enabled) ); stateManager.saveIfDirty(); player.sendMessage(ChatColor.YELLOW + "Base navigation is now " @@ -44,4 +56,18 @@ final class BaseNavigationCommand implements CommandExecutor { } return true; } + + @Override + public List onTabComplete( + CommandSender sender, + Command command, + String alias, + String[] arguments + ) { + if (arguments.length != 1) { + return List.of(); + } + String prefix = arguments[0].toLowerCase(Locale.ROOT); + return MODES.stream().filter(mode -> mode.startsWith(prefix)).toList(); + } } diff --git a/src/main/java/games/dmg/spigotbase/BaseProgressionService.java b/src/main/java/games/dmg/spigotbase/BaseProgressionService.java index 4253847..b85d6c9 100644 --- a/src/main/java/games/dmg/spigotbase/BaseProgressionService.java +++ b/src/main/java/games/dmg/spigotbase/BaseProgressionService.java @@ -25,7 +25,7 @@ public final class BaseProgressionService { } boolean unlocked = baseLevel != previousLevel; PlayerState updated = player.withGrassAndDirtProgress(count, baseLevel); - if (baseLevel >= 2 && !updated.navigationEnabled()) { + if (unlocked && baseLevel == 2) { updated = updated.withNavigationEnabled(true); } return new ProgressionUpdate(updated, unlocked, false, false, false, false); diff --git a/src/main/java/games/dmg/spigotbase/NavigationPreference.java b/src/main/java/games/dmg/spigotbase/NavigationPreference.java new file mode 100644 index 0000000..ad732a5 --- /dev/null +++ b/src/main/java/games/dmg/spigotbase/NavigationPreference.java @@ -0,0 +1,22 @@ +package games.dmg.spigotbase; + +import java.util.Locale; + +final class NavigationPreference { + private NavigationPreference() { + } + + static boolean resolve(boolean current, String[] arguments) { + if (arguments.length == 0) { + return !current; + } + if (arguments.length != 1) { + throw new IllegalArgumentException("expected zero or one argument"); + } + return switch (arguments[0].toLowerCase(Locale.ROOT)) { + case "on" -> true; + case "off" -> false; + default -> throw new IllegalArgumentException("expected on or off"); + }; + } +} diff --git a/src/main/java/games/dmg/spigotbase/SpigotBasePlugin.java b/src/main/java/games/dmg/spigotbase/SpigotBasePlugin.java index 5949d50..cd31cfa 100644 --- a/src/main/java/games/dmg/spigotbase/SpigotBasePlugin.java +++ b/src/main/java/games/dmg/spigotbase/SpigotBasePlugin.java @@ -71,7 +71,9 @@ public final class SpigotBasePlugin extends JavaPlugin { command("baseprogress").setExecutor( new BaseProgressCommand(stateManager, settingsProvider) ); - command("basenavigation").setExecutor(new BaseNavigationCommand(stateManager)); + BaseNavigationCommand navigationCommand = new BaseNavigationCommand(stateManager); + command("basenavigation").setExecutor(navigationCommand); + command("basenavigation").setTabCompleter(navigationCommand); command("baseflight").setExecutor(new BaseFlightCommand(stateManager, flightController)); command("basevisitors").setExecutor(new BaseVisitorsCommand(stateManager)); GoToBaseCommand goToBaseCommand = new GoToBaseCommand(stateManager, teleportManager); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 1d8ffbb..9c1efa1 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -13,7 +13,7 @@ commands: usage: /base [upgrade] basenavigation: description: Toggle particle navigation toward your base. - usage: /basenavigation + usage: /basenavigation [on|off] baseflight: description: Toggle flight within your base. usage: /baseflight diff --git a/src/test/java/games/dmg/spigotbase/BaseNavigationProgressionTest.java b/src/test/java/games/dmg/spigotbase/BaseNavigationProgressionTest.java index 50a65c6..8ef0779 100644 --- a/src/test/java/games/dmg/spigotbase/BaseNavigationProgressionTest.java +++ b/src/test/java/games/dmg/spigotbase/BaseNavigationProgressionTest.java @@ -1,6 +1,7 @@ package games.dmg.spigotbase; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Map; @@ -21,4 +22,18 @@ final class BaseNavigationProgressionTest { assertTrue(update.player().navigationEnabled()); assertTrue(update.unlockedBaseLevel()); } + + @Test + void laterProgressPreservesDisabledNavigationPreference() { + BaseProgressionService service = + new BaseProgressionService(PluginSettings.from(Map.of())); + PlayerState player = PlayerState.newPlayer(UUID.randomUUID(), "Alex") + .withGrassAndDirtProgress(500, 2) + .withNavigationEnabled(false); + + ProgressionUpdate update = service.recordGrassOrDirtBreak(player); + + assertFalse(update.player().navigationEnabled()); + assertFalse(update.unlockedBaseLevel()); + } } diff --git a/src/test/java/games/dmg/spigotbase/NavigationPreferenceTest.java b/src/test/java/games/dmg/spigotbase/NavigationPreferenceTest.java new file mode 100644 index 0000000..c55d249 --- /dev/null +++ b/src/test/java/games/dmg/spigotbase/NavigationPreferenceTest.java @@ -0,0 +1,35 @@ +package games.dmg.spigotbase; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +final class NavigationPreferenceTest { + @Test + void noArgumentTogglesCurrentPreference() { + assertFalse(NavigationPreference.resolve(true, new String[0])); + assertTrue(NavigationPreference.resolve(false, new String[0])); + } + + @Test + void explicitModesAreIdempotent() { + assertTrue(NavigationPreference.resolve(true, new String[] {"on"})); + assertTrue(NavigationPreference.resolve(false, new String[] {"on"})); + assertFalse(NavigationPreference.resolve(true, new String[] {"off"})); + assertFalse(NavigationPreference.resolve(false, new String[] {"off"})); + } + + @Test + void rejectsUnknownOrExtraArguments() { + assertThrows( + IllegalArgumentException.class, + () -> NavigationPreference.resolve(false, new String[] {"maybe"}) + ); + assertThrows( + IllegalArgumentException.class, + () -> NavigationPreference.resolve(false, new String[] {"on", "off"}) + ); + } +}