feat(vigilante): add follower control panel

This commit is contained in:
dmg
2026-08-21 21:19:29 -04:00
parent 62f6352633
commit ec7d50c2b5
25 changed files with 1087 additions and 4 deletions
@@ -0,0 +1,67 @@
package games.dmg.spigottyrant;
import static org.mockito.ArgumentMatchers.any;
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.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
final class DefaultVigilanteControlPanelTest {
@Test
void activeVigilanteOpensAuthoritativeOverview() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
GameState game = game(tyrantId, vigilanteId);
PlayerState vigilanteState = PlayerState.newPlayer(vigilanteId, "Vigilante");
TyrantStateManager manager = mock(TyrantStateManager.class);
when(manager.game()).thenReturn(game);
when(manager.players()).thenReturn(Map.of(vigilanteId, vigilanteState));
Player vigilante = mock(Player.class);
when(vigilante.getUniqueId()).thenReturn(vigilanteId);
VigilanteControlPanelRenderer renderer = mock(VigilanteControlPanelRenderer.class);
FollowerPresenceProvider presence = mock(FollowerPresenceProvider.class);
when(presence.inspect(any(), any())).thenReturn(Map.of());
DefaultVigilanteControlPanel panel = new DefaultVigilanteControlPanel(
manager, new FollowerService(), renderer, presence, 5, 4
);
panel.open(vigilante);
verify(renderer).openOverview(org.mockito.ArgumentMatchers.eq(vigilante), any());
}
@Test
void nonVigilanteCannotRenderControlPanel() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
Player other = mock(Player.class);
when(other.getUniqueId()).thenReturn(UUID.randomUUID());
TyrantStateManager manager = mock(TyrantStateManager.class);
when(manager.game()).thenReturn(game(tyrantId, vigilanteId));
VigilanteControlPanelRenderer renderer = mock(VigilanteControlPanelRenderer.class);
DefaultVigilanteControlPanel panel = new DefaultVigilanteControlPanel(
manager, new FollowerService(), renderer, mock(FollowerPresenceProvider.class), 5, 4
);
panel.open(other);
verify(renderer, never()).openOverview(any(), any());
verify(other).sendMessage(org.mockito.ArgumentMatchers.contains("active Vigilante"));
}
private static GameState game(UUID tyrantId, UUID vigilanteId) {
return new GameState(
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.of(vigilanteId),
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
0, 0, Set.of()
);
}
}
@@ -1,6 +1,7 @@
package games.dmg.spigottyrant;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.util.Map;
@@ -34,6 +35,32 @@ final class FollowerServiceTest {
assertEquals(TyrantClass.FIXER, accepted.players().get(FOLLOWER).tyrantClass());
}
@Test
void repeatedInvitationIsRejectedAndPendingInvitationCanBeInspected() {
GameState game = game();
PlayerState target = PlayerState.newPlayer(FOLLOWER, "Follower");
Map<UUID, PlayerState> players = Map.of(FOLLOWER, target);
FollowerResult first = service.invite(game, players, VIGILANTE, FOLLOWER);
FollowerResult repeated = service.invite(game, players, VIGILANTE, FOLLOWER);
assertEquals(FollowerStatus.INVITED, first.status());
assertEquals(FollowerStatus.ALREADY_INVITED, repeated.status());
assertTrue(service.invitedPlayerIds(VIGILANTE).contains(FOLLOWER));
}
@Test
void existingFollowerCannotReceiveAnotherInvitation() {
GameState game = game();
PlayerState follower = withFollower(PlayerState.newPlayer(FOLLOWER, "Follower"));
FollowerResult result = service.invite(
game, Map.of(FOLLOWER, follower), VIGILANTE, FOLLOWER
);
assertEquals(FollowerStatus.INELIGIBLE, result.status());
}
@Test
void vigilanteCanDismissAndFollowerCanLeave() {
GameState game = game();
@@ -0,0 +1,32 @@
package games.dmg.spigottyrant;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.UUID;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
final class VigilanteCommandTest {
@Test
void playerCanOpenControlPanelWithNoArgumentsOrMenuSubcommand() {
UUID playerId = UUID.fromString("22222222-2222-2222-2222-222222222222");
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId);
VigilanteControlPanel controlPanel = mock(VigilanteControlPanel.class);
VigilanteCommand command = new VigilanteCommand(
mock(TyrantStateManager.class),
new FollowerService(),
mock(OnlinePlayerDirectory.class),
controlPanel
);
command.onCommand(player, mock(Command.class), "vigilante", new String[0]);
command.onCommand(player, mock(Command.class), "vigilante", new String[] {"menu"});
verify(controlPanel, times(2)).open(player);
}
}
@@ -0,0 +1,80 @@
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.time.Duration;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerJoinEvent;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
final class VigilanteControlPanelJoinListenerTest {
@Test
void opensPanelOneHundredTicksAfterActiveVigilanteJoins() {
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
TyrantStateManager manager = mock(TyrantStateManager.class);
when(manager.game()).thenReturn(game(GameLifecycle.RUNNING, vigilanteId));
Player vigilante = player(vigilanteId);
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
when(event.getPlayer()).thenReturn(vigilante);
DelayedTaskScheduler scheduler = mock(DelayedTaskScheduler.class);
VigilanteControlPanel panel = mock(VigilanteControlPanel.class);
VigilanteControlPanelJoinListener listener = new VigilanteControlPanelJoinListener(
manager, panel, scheduler
);
listener.onJoin(event);
ArgumentCaptor<Runnable> task = ArgumentCaptor.forClass(Runnable.class);
verify(scheduler).schedule(task.capture(), org.mockito.ArgumentMatchers.eq(100L));
task.getValue().run();
verify(panel).open(vigilante);
}
@Test
void doesNotOpenAfterPauseOrWhenAlreadyOpenedManually() {
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
Player vigilante = player(vigilanteId);
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
when(event.getPlayer()).thenReturn(vigilante);
DelayedTaskScheduler scheduler = mock(DelayedTaskScheduler.class);
VigilanteControlPanel panel = mock(VigilanteControlPanel.class);
when(panel.openedThisSession(vigilanteId)).thenReturn(true);
TyrantStateManager manager = mock(TyrantStateManager.class);
when(manager.game()).thenReturn(game(GameLifecycle.PAUSED, vigilanteId));
VigilanteControlPanelJoinListener listener = new VigilanteControlPanelJoinListener(
manager, panel, scheduler
);
listener.onJoin(event);
ArgumentCaptor<Runnable> task = ArgumentCaptor.forClass(Runnable.class);
verify(scheduler).schedule(task.capture(), org.mockito.ArgumentMatchers.eq(100L));
task.getValue().run();
verify(panel, never()).open(vigilante);
}
private static Player player(UUID id) {
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(id);
when(player.isOnline()).thenReturn(true);
return player;
}
private static GameState game(GameLifecycle lifecycle, UUID vigilanteId) {
return new GameState(
lifecycle,
Optional.of(UUID.fromString("11111111-1111-1111-1111-111111111111")),
Optional.of(vigilanteId), Optional.empty(), Optional.empty(),
lifecycle == GameLifecycle.PAUSED
? Optional.of(java.time.Instant.EPOCH) : Optional.empty(),
Duration.ZERO, 0, 0, Set.of()
);
}
}
@@ -0,0 +1,58 @@
package games.dmg.spigottyrant;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
final class VigilanteControlPanelModelTest {
@Test
void overviewReportsFollowersInvitationsAndCappedNearbyPower() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
UUID nearbyId = UUID.fromString("33333333-3333-3333-3333-333333333333");
UUID offlineId = UUID.fromString("44444444-4444-4444-4444-444444444444");
UUID invitedId = UUID.fromString("55555555-5555-5555-5555-555555555555");
GameState game = new GameState(
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.of(vigilanteId),
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
0, 0, Set.of()
);
PlayerState nearby = follower(nearbyId, "Nearby", vigilanteId);
PlayerState offline = follower(offlineId, "Offline", vigilanteId);
PlayerState invited = PlayerState.newPlayer(invitedId, "Invited");
VigilanteControlPanelModel model = VigilanteControlPanelModel.create(
game,
Map.of(nearbyId, nearby, offlineId, offline, invitedId, invited),
Set.of(invitedId),
Map.of(
nearbyId, new FollowerPresence(true, true, true, true),
offlineId, new FollowerPresence(false, false, false, false)
),
5,
4
);
assertEquals(2, model.followers().size());
assertEquals(Set.of("Invited"), model.pendingInvitations());
assertEquals(1, model.nearbyEligibleFollowers());
assertEquals(1, model.strengthLevel());
assertEquals(1, model.resistanceLevel());
assertEquals(5, model.strengthCap());
assertEquals(4, model.resistanceCap());
}
private static PlayerState follower(UUID id, String name, UUID vigilanteId) {
PlayerState player = PlayerState.newPlayer(id, name);
return new PlayerState(
player.playerId(), player.latestName(), player.lastLogin(), player.optedOutUntil(),
player.tyrantClass(), Optional.of(vigilanteId), player.cooldownEnds(),
player.readyAbilityItems(), player.capturedMobs()
);
}
}