Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7318d4afa6 | ||
|
|
80810b3ba1 |
@@ -6,6 +6,30 @@ description: Chronological record of material decisions affecting the Spigot Tyr
|
||||
|
||||
# Spigot Tyrant Design Log
|
||||
|
||||
## 2026-08-21 — Contextual command completion completed
|
||||
|
||||
- Completed US-018 with registered completers for `/tyrant`, `/vigilante`, and `/tyrantadmin`, replacing Bukkit's unconditional online-player fallback.
|
||||
- Suggestions now follow argument position, filter case-insensitively, expose eligible assignment and recruitment targets, identify current Followers for dismissal, and gate administration by permission.
|
||||
- Administrative completion remains available to permitted console senders, while player-only commands return no console suggestions.
|
||||
- Verified syntax, eligibility, membership, permissions, prefix filtering, compiler warnings, tests, and packaging with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-21 — Contextual command completion started
|
||||
|
||||
- US-018 begins a test-first replacement of Bukkit's player-name fallback with syntax-aware Tyrant, Vigilante, and administrative command suggestions.
|
||||
- Player arguments will be filtered by the applicable assignment or membership rules, while administrative suggestions remain permission-gated.
|
||||
|
||||
## 2026-08-21 — Single-mob Tamer custody completed
|
||||
|
||||
- Completed US-007 with an authoritative one-mob custody limit that rejects a second capture before inventory, entity, or state mutation and tells the Tamer to release the held mob first.
|
||||
- Releasing or dropping custody permits another capture immediately, while durable state preserves the limit across logout, restart, recovery, and repeated interaction.
|
||||
- Legacy excess custody retains the oldest mob and safely releases extras; offline or failed releases remain durable for periodic retry.
|
||||
- Verified domain rejection, listener ordering, legacy reconciliation, compiler warnings, tests, and packaging with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-21 — Single-mob Tamer custody started
|
||||
|
||||
- US-007 begins a test-first one-mob custody limit for active Tamers.
|
||||
- New captures will be rejected before mutation while custody exists, and legacy excess custody will be released safely with failed releases retained for retry.
|
||||
|
||||
## 2026-08-21 — Former Tamer automatic release completed
|
||||
|
||||
- Completed US-004 and US-007 so every captured mob is released automatically after its holder loses the Tamer class, regardless of the class-removal path.
|
||||
|
||||
@@ -17,3 +17,4 @@
|
||||
15. [US-015: Manage a reign through the Tyrant control panel](us-015-manage-tyrant-control-panel.md)
|
||||
16. [US-016: Manage Followers through the Vigilante control panel](us-016-manage-vigilante-control-panel.md)
|
||||
17. [US-017: Use bound role control items](us-017-use-bound-role-control-items.md)
|
||||
18. [US-018: Complete Tyrant commands contextually](us-018-complete-commands-contextually.md)
|
||||
|
||||
@@ -28,10 +28,16 @@ As the **Tamer**, I want to capture a mob and release it elsewhere so that I can
|
||||
- [x] If the former Tamer is offline or no safe release location is available, release is deferred without losing the captured mob.
|
||||
- [x] A deferred release is retried when the former Tamer next logs in or reaches a safe location.
|
||||
- [x] Tyrant death, class reassignment, opt-out, administration, and other Tamer-removal paths use the same release behavior.
|
||||
- [x] A Tamer can hold custody of at most one captured mob at a time.
|
||||
- [x] Attempting to capture another mob while one is held is rejected before changing either mob.
|
||||
- [x] The Tamer is told to release the currently captured mob before capturing another.
|
||||
- [x] Releasing or dropping the held mob immediately allows another capture.
|
||||
- [x] The one-mob limit survives logout, restart, item recovery, and repeated or concurrent interaction.
|
||||
- [x] If legacy state contains multiple captured mobs, extras are safely released when possible; custody is retained until release succeeds so no mob is lost.
|
||||
|
||||
## Validation
|
||||
|
||||
Automated tests verify that only former Tamers are processed, offline custody remains deferred, successful releases remove only their corresponding custody records, and failed releases remain available for retry. The complete `./gradlew clean check jar` lifecycle passes.
|
||||
Automated tests verify that only former Tamers are processed, offline custody remains deferred, successful releases remove only their corresponding custody records, and failed releases remain available for retry. Additional tests verify pre-mutation rejection of a second capture, clear player guidance, and safe reduction of legacy excess custody. The complete `./gradlew clean check jar` lifecycle passes.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
type: User Story
|
||||
title: "US-018: Complete Tyrant commands contextually"
|
||||
description: Replace Bukkit's player-name fallback with valid context-aware command suggestions.
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-018: Complete Tyrant commands contextually
|
||||
|
||||
As a **player or administrator**, I want contextual command suggestions so that I can discover valid Tyrant commands and arguments.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `/tyrant` suggests its available subcommands instead of arbitrary player names.
|
||||
- [x] `/tyrant buy` suggests Tyrant unlock names.
|
||||
- [x] `/tyrant assign` suggests `assassin`, `fixer`, and `tamer`, followed by eligible online players.
|
||||
- [x] `/tyrant relinquish` suggests `confirm`.
|
||||
- [x] `/vigilante` suggests its available subcommands.
|
||||
- [x] `/vigilante invite` suggests eligible online recruitment candidates.
|
||||
- [x] `/vigilante dismiss` suggests current online Followers.
|
||||
- [x] `/tyrantadmin` suggestions are visible only to senders with `spigottyrant.admin`.
|
||||
- [x] `/tyrantadmin start` suggests online Tyrant and Vigilante candidates.
|
||||
- [x] `/tyrantadmin reset` suggests `confirm`.
|
||||
- [x] Suggestions are filtered case-insensitively by the partially typed argument.
|
||||
- [x] Suggestions never include syntactically invalid options for the current argument position.
|
||||
- [x] Console completion works for administrative commands without exposing player-only commands as executable console actions.
|
||||
- [x] Existing command execution behavior remains unchanged.
|
||||
|
||||
## Validation
|
||||
|
||||
Automated tests verify root syntax, unlocks, classes, confirmations, eligible recruits, current Followers, permission gating, online administrative candidates, argument positions, and case-insensitive prefix filtering. The complete `./gradlew clean check jar` lifecycle passes.
|
||||
|
||||
## Related
|
||||
|
||||
- [Start, pause, and administer the game](us-001-start-pause-and-administer.md)
|
||||
- [Assign unlocked classes](us-004-assign-unlocked-classes.md)
|
||||
- [Support the Vigilante and Followers](us-008-support-vigilante-and-followers.md)
|
||||
- [Manage a reign through the Tyrant control panel](us-015-manage-tyrant-control-panel.md)
|
||||
- [Manage Followers through the Vigilante control panel](us-016-manage-vigilante-control-panel.md)
|
||||
@@ -2,7 +2,8 @@ package games.dmg.spigottyrant;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CapturedMobCustodyRelease {
|
||||
PlayerState releaseAll(Player player, PlayerState state);
|
||||
|
||||
PlayerState releaseExcess(Player player, PlayerState state, int custodyLimit);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ public final class CapturedMobService {
|
||||
if (player.tyrantClass() != TyrantClass.TAMER) {
|
||||
throw new IllegalStateException("only a Tamer can capture mobs");
|
||||
}
|
||||
if (!player.capturedMobs().isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"release the currently captured mob before capturing another"
|
||||
);
|
||||
}
|
||||
if ("ENDER_DRAGON".equalsIgnoreCase(entityType)) {
|
||||
throw new IllegalArgumentException("Ender Dragons cannot be captured");
|
||||
}
|
||||
|
||||
@@ -13,13 +13,27 @@ public final class DefaultCapturedMobCustodyRelease implements CapturedMobCustod
|
||||
|
||||
@Override
|
||||
public PlayerState releaseAll(Player player, PlayerState state) {
|
||||
return releaseExcess(player, state, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayerState releaseExcess(
|
||||
Player player,
|
||||
PlayerState state,
|
||||
int custodyLimit
|
||||
) {
|
||||
if (custodyLimit < 0) {
|
||||
throw new IllegalArgumentException("custodyLimit must not be negative");
|
||||
}
|
||||
List<CapturedMob> retained = new ArrayList<>();
|
||||
for (CapturedMob mob : state.capturedMobs()) {
|
||||
if (!spawner.spawn(player, mob)) {
|
||||
List<CapturedMob> custody = state.capturedMobs();
|
||||
for (int index = 0; index < custody.size(); index++) {
|
||||
CapturedMob mob = custody.get(index);
|
||||
if (index < custodyLimit || !spawner.spawn(player, mob)) {
|
||||
retained.add(mob);
|
||||
}
|
||||
}
|
||||
if (retained.size() == state.capturedMobs().size()) {
|
||||
if (retained.equals(custody)) {
|
||||
return state;
|
||||
}
|
||||
return new PlayerState(
|
||||
|
||||
@@ -37,14 +37,18 @@ public final class FormerTamerReleaseTask implements Runnable {
|
||||
Map<UUID, Player> online = server.getOnlinePlayers().stream()
|
||||
.collect(Collectors.toMap(Player::getUniqueId, player -> player));
|
||||
for (PlayerState state : stateManager.players().values()) {
|
||||
if (state.tyrantClass() == TyrantClass.TAMER || state.capturedMobs().isEmpty()) {
|
||||
if (state.capturedMobs().isEmpty()
|
||||
|| state.tyrantClass() == TyrantClass.TAMER
|
||||
&& state.capturedMobs().size() == 1) {
|
||||
continue;
|
||||
}
|
||||
Player player = online.get(state.playerId());
|
||||
if (player == null || player.isDead()) {
|
||||
continue;
|
||||
}
|
||||
PlayerState released = release.releaseAll(player, state);
|
||||
PlayerState released = state.tyrantClass() == TyrantClass.TAMER
|
||||
? release.releaseExcess(player, state, 1)
|
||||
: release.releaseAll(player, state);
|
||||
if (released.equals(state)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -136,6 +136,14 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
settings.pendingSelectionTimeout(),
|
||||
random
|
||||
));
|
||||
Objects.requireNonNull(getCommand("tyrant"), "Missing tyrant command metadata")
|
||||
.setTabCompleter(new TyrantTabCompleter(stateManager, onlinePlayers));
|
||||
Objects.requireNonNull(getCommand("vigilante"), "Missing vigilante metadata")
|
||||
.setTabCompleter(new VigilanteTabCompleter(
|
||||
stateManager, followers, onlinePlayers
|
||||
));
|
||||
Objects.requireNonNull(getCommand("tyrantadmin"), "Missing tyrantadmin metadata")
|
||||
.setTabCompleter(new TyrantAdminTabCompleter(onlinePlayers));
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new PlayerJoinListener(stateManager, clock, tyrantPresentation),
|
||||
this
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class TabSuggestions {
|
||||
private TabSuggestions() {
|
||||
}
|
||||
|
||||
public static List<String> matching(String prefix, Collection<String> candidates) {
|
||||
String normalized = prefix == null ? "" : prefix.toLowerCase(Locale.ROOT);
|
||||
return candidates.stream()
|
||||
.filter(candidate -> candidate.toLowerCase(Locale.ROOT).startsWith(normalized))
|
||||
.distinct()
|
||||
.sorted(String.CASE_INSENSITIVE_ORDER)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,11 @@ public final class TamerListener implements Listener {
|
||||
return;
|
||||
}
|
||||
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
|
||||
if (!state.capturedMobs().isEmpty()) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "Release the currently captured mob before capturing another.");
|
||||
return;
|
||||
}
|
||||
Entity target = event.getRightClicked();
|
||||
if (!canCapture(state, target)) {
|
||||
player.sendMessage(ChatColor.RED + "That mob cannot be captured.");
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class TyrantAdminTabCompleter implements TabCompleter {
|
||||
private static final String PERMISSION = "spigottyrant.admin";
|
||||
private static final List<String> SUBCOMMANDS = List.of(
|
||||
"status", "start", "pause", "resume", "reset"
|
||||
);
|
||||
private final OnlinePlayerDirectory onlinePlayers;
|
||||
|
||||
public TyrantAdminTabCompleter(OnlinePlayerDirectory onlinePlayers) {
|
||||
this.onlinePlayers = onlinePlayers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(
|
||||
CommandSender sender,
|
||||
Command command,
|
||||
String alias,
|
||||
String[] arguments
|
||||
) {
|
||||
if (!sender.hasPermission(PERMISSION) || arguments.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
if (arguments.length == 1) {
|
||||
return TabSuggestions.matching(arguments[0], SUBCOMMANDS);
|
||||
}
|
||||
String subcommand = arguments[0].toLowerCase(java.util.Locale.ROOT);
|
||||
if (arguments.length == 2) {
|
||||
return switch (subcommand) {
|
||||
case "start" -> TabSuggestions.matching(arguments[1], onlineNames(null));
|
||||
case "reset" -> TabSuggestions.matching(arguments[1], List.of("confirm"));
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
if (arguments.length == 3 && subcommand.equals("start")) {
|
||||
return TabSuggestions.matching(arguments[2], onlineNames(arguments[1]));
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> onlineNames(String excludedName) {
|
||||
List<String> names = new ArrayList<>();
|
||||
for (UUID playerId : onlinePlayers.onlinePlayerIds()) {
|
||||
Player player = onlinePlayers.findById(playerId);
|
||||
if (player != null && (excludedName == null
|
||||
|| !player.getName().equalsIgnoreCase(excludedName))) {
|
||||
names.add(player.getName());
|
||||
}
|
||||
}
|
||||
return List.copyOf(names);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class TyrantTabCompleter implements TabCompleter {
|
||||
private static final List<String> SUBCOMMANDS = List.of(
|
||||
"menu", "status", "choices", "buy", "assign", "item", "intelligence",
|
||||
"optout", "optin", "relinquish"
|
||||
);
|
||||
private static final List<String> UNLOCKS = java.util.Arrays.stream(TyrantUnlock.values())
|
||||
.map(value -> value.name().toLowerCase(java.util.Locale.ROOT))
|
||||
.toList();
|
||||
private static final List<String> CLASSES = List.of("assassin", "fixer", "tamer");
|
||||
private final TyrantStateManager stateManager;
|
||||
private final OnlinePlayerDirectory onlinePlayers;
|
||||
|
||||
public TyrantTabCompleter(
|
||||
TyrantStateManager stateManager,
|
||||
OnlinePlayerDirectory onlinePlayers
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.onlinePlayers = onlinePlayers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(
|
||||
CommandSender sender,
|
||||
Command command,
|
||||
String alias,
|
||||
String[] arguments
|
||||
) {
|
||||
if (!(sender instanceof Player player) || arguments.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
if (arguments.length == 1) {
|
||||
return TabSuggestions.matching(arguments[0], SUBCOMMANDS);
|
||||
}
|
||||
String subcommand = arguments[0].toLowerCase(java.util.Locale.ROOT);
|
||||
if (arguments.length == 2) {
|
||||
return switch (subcommand) {
|
||||
case "buy" -> TabSuggestions.matching(arguments[1], UNLOCKS);
|
||||
case "assign" -> TabSuggestions.matching(arguments[1], CLASSES);
|
||||
case "relinquish" -> TabSuggestions.matching(
|
||||
arguments[1], List.of("confirm")
|
||||
);
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
if (arguments.length == 3 && subcommand.equals("assign")) {
|
||||
return TabSuggestions.matching(arguments[2], eligibleAssignees(player));
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> eligibleAssignees(Player tyrant) {
|
||||
Map<UUID, PlayerState> states = stateManager.players();
|
||||
GameState game = stateManager.game();
|
||||
Set<UUID> online = onlinePlayers.onlinePlayerIds();
|
||||
List<String> names = new ArrayList<>();
|
||||
for (UUID playerId : online) {
|
||||
if (playerId.equals(tyrant.getUniqueId())
|
||||
|| game != null && game.tyrantId().filter(playerId::equals).isPresent()) {
|
||||
continue;
|
||||
}
|
||||
PlayerState state = states.get(playerId);
|
||||
if (state != null && state.optedOutUntil().isPresent()) {
|
||||
continue;
|
||||
}
|
||||
Player candidate = onlinePlayers.findById(playerId);
|
||||
if (candidate != null) {
|
||||
names.add(candidate.getName());
|
||||
}
|
||||
}
|
||||
return List.copyOf(names);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class VigilanteTabCompleter implements TabCompleter {
|
||||
private static final List<String> SUBCOMMANDS = List.of(
|
||||
"menu", "item", "invite", "accept", "dismiss", "leave"
|
||||
);
|
||||
private final TyrantStateManager stateManager;
|
||||
private final FollowerService followers;
|
||||
private final OnlinePlayerDirectory onlinePlayers;
|
||||
|
||||
public VigilanteTabCompleter(
|
||||
TyrantStateManager stateManager,
|
||||
FollowerService followers,
|
||||
OnlinePlayerDirectory onlinePlayers
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.followers = followers;
|
||||
this.onlinePlayers = onlinePlayers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(
|
||||
CommandSender sender,
|
||||
Command command,
|
||||
String alias,
|
||||
String[] arguments
|
||||
) {
|
||||
if (!(sender instanceof Player player) || arguments.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
if (arguments.length == 1) {
|
||||
return TabSuggestions.matching(arguments[0], SUBCOMMANDS);
|
||||
}
|
||||
if (arguments.length != 2) {
|
||||
return List.of();
|
||||
}
|
||||
return switch (arguments[0].toLowerCase(java.util.Locale.ROOT)) {
|
||||
case "invite" -> TabSuggestions.matching(
|
||||
arguments[1], eligibleInvitees(player)
|
||||
);
|
||||
case "dismiss" -> TabSuggestions.matching(
|
||||
arguments[1], currentFollowers(player)
|
||||
);
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
private List<String> eligibleInvitees(Player vigilante) {
|
||||
GameState game = stateManager.game();
|
||||
Map<UUID, PlayerState> states = stateManager.players();
|
||||
Set<UUID> pending = followers.invitedPlayerIds(vigilante.getUniqueId());
|
||||
List<String> names = new ArrayList<>();
|
||||
for (UUID playerId : onlinePlayers.onlinePlayerIds()) {
|
||||
PlayerState state = states.get(playerId);
|
||||
boolean assignedRole = game.tyrantId().filter(playerId::equals).isPresent()
|
||||
|| game.vigilanteId().filter(playerId::equals).isPresent();
|
||||
boolean ineligibleState = state != null
|
||||
&& (state.optedOutUntil().isPresent() || state.followerOf().isPresent());
|
||||
if (assignedRole || ineligibleState || pending.contains(playerId)) {
|
||||
continue;
|
||||
}
|
||||
Player candidate = onlinePlayers.findById(playerId);
|
||||
if (candidate != null) {
|
||||
names.add(candidate.getName());
|
||||
}
|
||||
}
|
||||
return List.copyOf(names);
|
||||
}
|
||||
|
||||
private List<String> currentFollowers(Player vigilante) {
|
||||
Map<UUID, PlayerState> states = stateManager.players();
|
||||
List<String> names = new ArrayList<>();
|
||||
for (UUID playerId : onlinePlayers.onlinePlayerIds()) {
|
||||
PlayerState state = states.get(playerId);
|
||||
if (state == null
|
||||
|| state.followerOf().filter(vigilante.getUniqueId()::equals).isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Player candidate = onlinePlayers.findById(playerId);
|
||||
if (candidate != null) {
|
||||
names.add(candidate.getName());
|
||||
}
|
||||
}
|
||||
return List.copyOf(names);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,21 @@ final class CapturedMobServiceTest {
|
||||
assertEquals(java.util.List.of(), released.capturedMobs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void tamerMustReleaseHeldMobBeforeCapturingAnother() {
|
||||
PlayerState holding = service.capture(
|
||||
tamer(), UUID.randomUUID(), "COW", "cow-snapshot"
|
||||
);
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class, () ->
|
||||
service.capture(holding, UUID.randomUUID(), "SHEEP", "sheep-snapshot")
|
||||
);
|
||||
|
||||
assertEquals("release the currently captured mob before capturing another",
|
||||
exception.getMessage());
|
||||
assertEquals(1, holding.capturedMobs().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void enderDragonCanNeverBeCapturedAndMissingReleaseDoesNotMutate() {
|
||||
assertThrows(IllegalArgumentException.class, () -> service.capture(
|
||||
|
||||
@@ -12,6 +12,28 @@ import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class DefaultCapturedMobCustodyReleaseTest {
|
||||
@Test
|
||||
void legacyExcessReleaseRetainsOneAndAnyFailedExtra() {
|
||||
UUID playerId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
CapturedMob retained = mob("COW");
|
||||
CapturedMob releasedExtra = mob("SHEEP");
|
||||
CapturedMob deferredExtra = mob("PIG");
|
||||
PlayerState state = new PlayerState(
|
||||
playerId, "Tamer", Optional.empty(), Optional.empty(), TyrantClass.TAMER,
|
||||
Optional.empty(), Map.of(), java.util.Set.of(),
|
||||
List.of(retained, releasedExtra, deferredExtra)
|
||||
);
|
||||
CapturedMobSpawner spawner = mock(CapturedMobSpawner.class);
|
||||
Player player = mock(Player.class);
|
||||
when(spawner.spawn(player, releasedExtra)).thenReturn(true);
|
||||
when(spawner.spawn(player, deferredExtra)).thenReturn(false);
|
||||
DefaultCapturedMobCustodyRelease service = new DefaultCapturedMobCustodyRelease(spawner);
|
||||
|
||||
PlayerState result = service.releaseExcess(player, state, 1);
|
||||
|
||||
assertEquals(List.of(retained, deferredExtra), result.capturedMobs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removesOnlyCustodyThatSpawnedSuccessfully() {
|
||||
UUID playerId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
|
||||
@@ -31,6 +31,28 @@ final class FormerTamerReleaseTaskTest {
|
||||
verify(release, never()).releaseAll(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void safelyReleasesLegacyCustodyAboveOneForActiveTamer() {
|
||||
UUID tamerId = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||
PlayerState once = withMob(withClass(
|
||||
PlayerState.newPlayer(tamerId, "Tamer"), TyrantClass.TAMER
|
||||
));
|
||||
PlayerState legacy = withMob(once);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.players()).thenReturn(Map.of(tamerId, legacy));
|
||||
Player tamer = mock(Player.class);
|
||||
when(tamer.getUniqueId()).thenReturn(tamerId);
|
||||
Server server = mock(Server.class);
|
||||
doReturn(Set.of(tamer)).when(server).getOnlinePlayers();
|
||||
CapturedMobCustodyRelease release = mock(CapturedMobCustodyRelease.class);
|
||||
when(release.releaseExcess(tamer, legacy, 1)).thenReturn(legacy);
|
||||
|
||||
new FormerTamerReleaseTask(manager, server, release).run();
|
||||
|
||||
verify(release).releaseExcess(tamer, legacy, 1);
|
||||
verify(release, never()).releaseAll(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasesCustodyOnlyAfterPlayerLosesTamerClass() {
|
||||
UUID formerId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
@@ -61,10 +83,12 @@ final class FormerTamerReleaseTaskTest {
|
||||
CapturedMob mob = new CapturedMob("COW", Map.of(
|
||||
"capture-id", UUID.randomUUID().toString(), "snapshot", "snapshot"
|
||||
));
|
||||
java.util.List<CapturedMob> custody = new java.util.ArrayList<>(player.capturedMobs());
|
||||
custody.add(mob);
|
||||
return new PlayerState(
|
||||
player.playerId(), player.latestName(), player.lastLogin(), player.optedOutUntil(),
|
||||
player.tyrantClass(), player.followerOf(), player.cooldownEnds(),
|
||||
player.readyAbilityItems(), java.util.List.of(mob)
|
||||
player.readyAbilityItems(), custody
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TamerListenerTest {
|
||||
@Test
|
||||
void heldMobRejectsAnotherCaptureBeforeTargetOrInventoryChanges() {
|
||||
UUID playerId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
CapturedMob heldMob = new CapturedMob("COW", Map.of(
|
||||
"capture-id", UUID.randomUUID().toString(), "snapshot", "cow"
|
||||
));
|
||||
PlayerState state = new PlayerState(
|
||||
playerId, "Tamer", Optional.empty(), Optional.empty(), TyrantClass.TAMER,
|
||||
Optional.empty(), Map.of(), Set.of(Ability.TAMER_CAPTURE),
|
||||
java.util.List.of(heldMob)
|
||||
);
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Tamer");
|
||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
ItemStack lead = mock(ItemStack.class);
|
||||
when(inventory.getItem(org.bukkit.inventory.EquipmentSlot.HAND)).thenReturn(lead);
|
||||
Entity target = mock(Entity.class);
|
||||
PlayerInteractEntityEvent event = mock(PlayerInteractEntityEvent.class);
|
||||
when(event.getPlayer()).thenReturn(player);
|
||||
when(event.getHand()).thenReturn(org.bukkit.inventory.EquipmentSlot.HAND);
|
||||
when(event.getRightClicked()).thenReturn(target);
|
||||
AbilityItemService abilityItems = mock(AbilityItemService.class);
|
||||
when(abilityItems.ability(lead)).thenReturn(Optional.of(Ability.TAMER_CAPTURE));
|
||||
when(abilityItems.owner(lead)).thenReturn(Optional.of(playerId));
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.player(playerId, "Tamer")).thenReturn(state);
|
||||
CapturedMobItemService capturedItems = mock(CapturedMobItemService.class);
|
||||
TamerListener listener = new TamerListener(
|
||||
manager, abilityItems, capturedItems, new CapturedMobService(),
|
||||
PluginSettings.from(Map.of()), mock(Server.class)
|
||||
);
|
||||
|
||||
listener.onCapture(event);
|
||||
|
||||
verify(player).sendMessage(org.mockito.ArgumentMatchers.contains(
|
||||
"Release the currently captured mob"
|
||||
));
|
||||
verify(target, never()).createSnapshot();
|
||||
verify(capturedItems, never()).create(
|
||||
org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.anyString()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TyrantAdminTabCompleterTest {
|
||||
@Test
|
||||
void permissionGatesAdministrativeSyntaxAndOnlineCandidates() {
|
||||
UUID alphaId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID betaId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
Player alpha = player(alphaId, "Alpha");
|
||||
Player beta = player(betaId, "Beta");
|
||||
OnlinePlayerDirectory directory = mock(OnlinePlayerDirectory.class);
|
||||
when(directory.onlinePlayerIds()).thenReturn(Set.of(alphaId, betaId));
|
||||
when(directory.findById(alphaId)).thenReturn(alpha);
|
||||
when(directory.findById(betaId)).thenReturn(beta);
|
||||
TyrantAdminTabCompleter completer = new TyrantAdminTabCompleter(directory);
|
||||
Command command = mock(Command.class);
|
||||
CommandSender denied = mock(CommandSender.class);
|
||||
CommandSender admin = mock(CommandSender.class);
|
||||
when(admin.hasPermission("spigottyrant.admin")).thenReturn(true);
|
||||
|
||||
assertEquals(java.util.List.of(), completer.onTabComplete(
|
||||
denied, command, "tyrantadmin", new String[] {""}
|
||||
));
|
||||
assertTrue(completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {""}
|
||||
).containsAll(java.util.List.of("status", "start", "pause", "resume", "reset")));
|
||||
assertEquals(java.util.List.of("Alpha", "Beta"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"start", ""}
|
||||
));
|
||||
assertEquals(java.util.List.of("Beta"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"start", "Alpha", ""}
|
||||
));
|
||||
assertEquals(java.util.List.of("confirm"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"reset", "c"}
|
||||
));
|
||||
}
|
||||
|
||||
private static Player player(UUID id, String name) {
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(id);
|
||||
when(player.getName()).thenReturn(name);
|
||||
return player;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
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 static org.mockito.Mockito.mock;
|
||||
|
||||
import java.util.List;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TyrantTabCompleterTest {
|
||||
@Test
|
||||
void rootBuyAssignAndConfirmationSuggestionsFollowSyntaxAndPrefix() {
|
||||
TyrantTabCompleter completer = new TyrantTabCompleter(
|
||||
mock(TyrantStateManager.class), mock(OnlinePlayerDirectory.class)
|
||||
);
|
||||
Player player = mock(Player.class);
|
||||
Command command = mock(Command.class);
|
||||
|
||||
List<String> root = completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {""}
|
||||
);
|
||||
List<String> buy = completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {"buy", "ro"}
|
||||
);
|
||||
List<String> assign = completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {"assign", "f"}
|
||||
);
|
||||
List<String> confirm = completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {"relinquish", "c"}
|
||||
);
|
||||
|
||||
assertTrue(root.containsAll(List.of("menu", "status", "buy", "assign", "item")));
|
||||
assertFalse(root.contains("SomePlayer"));
|
||||
assertEquals(List.of("roster_intelligence"), buy);
|
||||
assertEquals(List.of("fixer"), assign);
|
||||
assertEquals(List.of("confirm"), confirm);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class VigilanteTabCompleterTest {
|
||||
@Test
|
||||
void rootAndPlayerArgumentsUseMembershipEligibility() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
UUID eligibleId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
UUID followerId = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||
PlayerState eligible = PlayerState.newPlayer(eligibleId, "Eligible");
|
||||
PlayerState follower = follower(followerId, "Follower", vigilanteId);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.of(vigilanteId),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
0, 0, Set.of()
|
||||
));
|
||||
when(manager.players()).thenReturn(Map.of(eligibleId, eligible, followerId, follower));
|
||||
OnlinePlayerDirectory directory = mock(OnlinePlayerDirectory.class);
|
||||
when(directory.onlinePlayerIds()).thenReturn(Set.of(eligibleId, followerId, tyrantId));
|
||||
Player eligiblePlayer = player(eligibleId, "Eligible");
|
||||
Player followerPlayer = player(followerId, "Follower");
|
||||
Player tyrantPlayer = player(tyrantId, "Tyrant");
|
||||
when(directory.findById(eligibleId)).thenReturn(eligiblePlayer);
|
||||
when(directory.findById(followerId)).thenReturn(followerPlayer);
|
||||
when(directory.findById(tyrantId)).thenReturn(tyrantPlayer);
|
||||
VigilanteTabCompleter completer = new VigilanteTabCompleter(
|
||||
manager, new FollowerService(), directory
|
||||
);
|
||||
Player vigilante = player(vigilanteId, "Vigilante");
|
||||
Command command = mock(Command.class);
|
||||
|
||||
assertTrue(completer.onTabComplete(
|
||||
vigilante, command, "vigilante", new String[] {""}
|
||||
).containsAll(java.util.List.of("menu", "item", "invite", "dismiss")));
|
||||
assertEquals(java.util.List.of("Eligible"), completer.onTabComplete(
|
||||
vigilante, command, "vigilante", new String[] {"invite", ""}
|
||||
));
|
||||
assertEquals(java.util.List.of("Follower"), completer.onTabComplete(
|
||||
vigilante, command, "vigilante", new String[] {"dismiss", ""}
|
||||
));
|
||||
}
|
||||
|
||||
private static Player player(UUID id, String name) {
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(id);
|
||||
when(player.getName()).thenReturn(name);
|
||||
return player;
|
||||
}
|
||||
|
||||
private static PlayerState follower(UUID id, String name, UUID vigilanteId) {
|
||||
PlayerState state = PlayerState.newPlayer(id, name);
|
||||
return new PlayerState(
|
||||
id, name, state.lastLogin(), state.optedOutUntil(), state.tyrantClass(),
|
||||
Optional.of(vigilanteId), state.cooldownEnds(), state.readyAbilityItems(),
|
||||
state.capturedMobs()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user