feat(admin): add Tyrant point grant command
This commit is contained in:
@@ -17,6 +17,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
private final ManagedRoleEffects effects;
|
||||
private final Clock clock;
|
||||
private final ArenaLocationStore arenaLocations;
|
||||
private final TyrantProgressionService progression = new TyrantProgressionService();
|
||||
|
||||
public TyrantAdminCommand(
|
||||
TyrantStateManager stateManager,
|
||||
@@ -69,6 +70,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
case "resume" -> resume(sender);
|
||||
case "reset" -> reset(sender, arguments);
|
||||
case "arena" -> arena(sender, arguments);
|
||||
case "grantpoint" -> grantPoint(sender, arguments);
|
||||
default -> usage(sender);
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
@@ -141,6 +143,20 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
sender.sendMessage(ChatColor.GREEN + "Tyrant game reset.");
|
||||
}
|
||||
|
||||
private void grantPoint(CommandSender sender, String[] arguments) {
|
||||
if (arguments.length != 1) {
|
||||
sender.sendMessage(ChatColor.YELLOW + "Usage: /tyrantadmin grantpoint");
|
||||
return;
|
||||
}
|
||||
PersistentState snapshot = stateManager.snapshot();
|
||||
GameState updated = progression.grantPoint(snapshot.game());
|
||||
stateManager.replaceState(new LifecycleState(updated, snapshot.players()));
|
||||
stateManager.saveIfDirty();
|
||||
sender.sendMessage(ChatColor.GREEN + "Granted the Tyrant one point; now level "
|
||||
+ updated.tyrantLevel() + " and " + updated.unspentChoices()
|
||||
+ " unspent choices.");
|
||||
}
|
||||
|
||||
private void arena(CommandSender sender, String[] arguments) {
|
||||
if (arguments.length == 1 || arguments[1].equalsIgnoreCase("status")) {
|
||||
sender.sendMessage("Shared role arena: " + arenaLocations.location()
|
||||
@@ -194,6 +210,6 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
|
||||
private static void usage(CommandSender sender) {
|
||||
sender.sendMessage(ChatColor.YELLOW
|
||||
+ "Usage: /tyrantadmin <status|start|pause|resume|reset confirm|arena set>");
|
||||
+ "Usage: /tyrantadmin <status|start|pause|resume|reset confirm|arena set|grantpoint>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ 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", "arena"
|
||||
"status", "start", "pause", "resume", "reset", "arena", "grantpoint"
|
||||
);
|
||||
private final OnlinePlayerDirectory onlinePlayers;
|
||||
|
||||
|
||||
@@ -20,6 +20,25 @@ public final class TyrantProgressionService {
|
||||
);
|
||||
}
|
||||
|
||||
public GameState grantPoint(GameState state) {
|
||||
if (state.lifecycle() != GameLifecycle.RUNNING) {
|
||||
throw new IllegalStateException("Tyrant game must be running.");
|
||||
}
|
||||
if (state.tyrantId().isEmpty()) {
|
||||
throw new IllegalStateException("There is no current Tyrant.");
|
||||
}
|
||||
if (state.tyrantLevel() == Integer.MAX_VALUE
|
||||
|| state.unspentChoices() == Integer.MAX_VALUE) {
|
||||
throw new IllegalStateException("Tyrant progression cannot be increased further.");
|
||||
}
|
||||
return copyProgress(
|
||||
state,
|
||||
state.tyrantLevel() + 1,
|
||||
state.unspentChoices() + 1,
|
||||
state.purchases()
|
||||
);
|
||||
}
|
||||
|
||||
public PurchaseResult purchase(
|
||||
GameState state,
|
||||
UUID buyerId,
|
||||
|
||||
@@ -13,7 +13,7 @@ commands:
|
||||
usage: /vigilante <menu|item|invite <player>|accept|dismiss <player>|leave>
|
||||
tyrantadmin:
|
||||
description: Administer the Spigot Tyrant game.
|
||||
usage: /tyrantadmin <status|start|pause|resume|reset confirm|arena <set|status>>
|
||||
usage: /tyrantadmin <status|start|pause|resume|reset confirm|arena <set|status>|grantpoint>
|
||||
permission: spigottyrant.admin
|
||||
permissions:
|
||||
spigottyrant.admin:
|
||||
|
||||
@@ -62,6 +62,93 @@ final class TyrantAdminCommandTest {
|
||||
verify(effects).suppressAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void grantPointAddsAndPersistsOneLevelAndChoice() {
|
||||
UUID tyrant = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrant), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
2, 1, Set.of(TyrantUnlock.ASSASSIN)
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.snapshot()).thenReturn(new PersistentState(
|
||||
game, Map.of(tyrant, PlayerState.newPlayer(tyrant, "Tyrant"))
|
||||
));
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigottyrant.admin")).thenReturn(true);
|
||||
|
||||
command(manager, mock(ManagedRoleEffects.class)).onCommand(
|
||||
sender, mock(Command.class), "tyrantadmin", new String[] {"grantpoint"}
|
||||
);
|
||||
|
||||
verify(manager).replaceState(org.mockito.ArgumentMatchers.argThat(state ->
|
||||
state.game().tyrantLevel() == 3
|
||||
&& state.game().unspentChoices() == 2
|
||||
&& state.game().purchases().equals(Set.of(TyrantUnlock.ASSASSIN))
|
||||
));
|
||||
verify(manager).saveIfDirty();
|
||||
verify(sender).sendMessage(contains("level 3 and 2 unspent choices"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void grantPointRejectsPausedGameWithoutChangingState() {
|
||||
UUID tyrant = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
GameState paused = new GameState(
|
||||
GameLifecycle.PAUSED, Optional.of(tyrant), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(),
|
||||
Optional.of(Instant.parse("2026-08-14T12:00:00Z")), Duration.ZERO,
|
||||
2, 1, Set.of()
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.snapshot()).thenReturn(new PersistentState(
|
||||
paused, Map.of(tyrant, PlayerState.newPlayer(tyrant, "Tyrant"))
|
||||
));
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigottyrant.admin")).thenReturn(true);
|
||||
|
||||
command(manager, mock(ManagedRoleEffects.class)).onCommand(
|
||||
sender, mock(Command.class), "tyrantadmin", new String[] {"grantpoint"}
|
||||
);
|
||||
|
||||
verify(manager, never()).replaceState(org.mockito.ArgumentMatchers.any());
|
||||
verify(sender).sendMessage(contains("must be running"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void grantPointRejectsAdditionalArgumentsWithoutChangingState() {
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigottyrant.admin")).thenReturn(true);
|
||||
|
||||
command(manager, mock(ManagedRoleEffects.class)).onCommand(
|
||||
sender, mock(Command.class), "tyrantadmin",
|
||||
new String[] {"grantpoint", "unexpected"}
|
||||
);
|
||||
|
||||
verify(manager, never()).replaceState(org.mockito.ArgumentMatchers.any());
|
||||
verify(sender).sendMessage(contains("Usage: /tyrantadmin grantpoint"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void grantPointRejectsTyrantVacancyWithoutChangingState() {
|
||||
GameState vacant = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
0, 0, Set.of()
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.snapshot()).thenReturn(new PersistentState(vacant, Map.of()));
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission("spigottyrant.admin")).thenReturn(true);
|
||||
|
||||
command(manager, mock(ManagedRoleEffects.class)).onCommand(
|
||||
sender, mock(Command.class), "tyrantadmin", new String[] {"grantpoint"}
|
||||
);
|
||||
|
||||
verify(manager, never()).replaceState(org.mockito.ArgumentMatchers.any());
|
||||
verify(sender).sendMessage(contains("no current Tyrant"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void playerCanPersistArenaAtCurrentLocation() {
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
|
||||
@@ -35,7 +35,7 @@ final class TyrantAdminTabCompleterTest {
|
||||
assertTrue(completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {""}
|
||||
).containsAll(java.util.List.of(
|
||||
"status", "start", "pause", "resume", "reset", "arena"
|
||||
"status", "start", "pause", "resume", "reset", "arena", "grantpoint"
|
||||
)));
|
||||
assertEquals(java.util.List.of("Alpha", "Beta"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"start", ""}
|
||||
@@ -49,6 +49,9 @@ final class TyrantAdminTabCompleterTest {
|
||||
assertEquals(java.util.List.of("set", "status"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"arena", "s"}
|
||||
));
|
||||
assertEquals(java.util.List.of(), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"grantpoint", ""}
|
||||
));
|
||||
}
|
||||
|
||||
private static Player player(UUID id, String name) {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -47,6 +48,38 @@ final class TyrantProgressionServiceTest {
|
||||
assertEquals(paused, service.recordVigilanteDeath(paused, Optional.of(TYRANT)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void administrativePointGrantAddsOneLevelAndChoicePerInvocation() {
|
||||
GameState before = runningState(2, 1, Set.of(TyrantUnlock.ASSASSIN));
|
||||
|
||||
GameState first = service.grantPoint(before);
|
||||
GameState second = service.grantPoint(first);
|
||||
|
||||
assertEquals(4, second.tyrantLevel());
|
||||
assertEquals(3, second.unspentChoices());
|
||||
assertEquals(before.purchases(), second.purchases());
|
||||
assertEquals(before.tyrantId(), second.tyrantId());
|
||||
assertEquals(before.vigilanteId(), second.vigilanteId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void administrativePointGrantRequiresRunningGameAndCurrentTyrant() {
|
||||
GameState running = runningState(0, 1, Set.of());
|
||||
GameState paused = new GameState(
|
||||
GameLifecycle.PAUSED, running.tyrantId(), running.vigilanteId(),
|
||||
running.pendingTyrant(), running.pendingVigilante(),
|
||||
Optional.of(java.time.Instant.parse("2026-08-14T12:00:00Z")),
|
||||
Duration.ZERO, 0, 1, Set.of()
|
||||
);
|
||||
GameState vacant = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Duration.ZERO, 0, 0, Set.of()
|
||||
);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> service.grantPoint(paused));
|
||||
assertThrows(IllegalStateException.class, () -> service.grantPoint(vacant));
|
||||
}
|
||||
|
||||
@Test
|
||||
void purchaseConsumesOneChoiceAndCannotBeBoughtTwice() {
|
||||
GameState before = runningState(1, 2, Set.of());
|
||||
|
||||
Reference in New Issue
Block a user