feat(tyrant): add reign control panel
Release / release (push) Successful in 2m42s
CI / build (push) Successful in 1m10s

This commit is contained in:
dmg
2026-08-21 20:47:33 -04:00
parent 3d65576010
commit 62f6352633
21 changed files with 1034 additions and 5 deletions
@@ -0,0 +1,72 @@
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.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
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 DefaultTyrantControlPanelTest {
@Test
void nonTyrantCannotRenderControlPanel() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
UUID otherId = UUID.fromString("22222222-2222-2222-2222-222222222222");
GameState game = new GameState(
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
0, 0, Set.of()
);
TyrantStateManager manager = mock(TyrantStateManager.class);
when(manager.game()).thenReturn(game);
Player other = mock(Player.class);
when(other.getUniqueId()).thenReturn(otherId);
TyrantControlPanelRenderer renderer = mock(TyrantControlPanelRenderer.class);
DefaultTyrantControlPanel panel = new DefaultTyrantControlPanel(
manager, renderer,
Clock.fixed(Instant.parse("2026-08-21T12:00:00Z"), ZoneOffset.UTC)
);
panel.open(other);
verify(renderer, never()).openOverview(any(), any());
verify(other).sendMessage(org.mockito.ArgumentMatchers.contains("active Tyrant"));
}
@Test
void openingAsActiveTyrantRendersAuthoritativeOverview() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
GameState game = new GameState(
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
2, 1, Set.of(TyrantUnlock.STRENGTH)
);
PlayerState tyrantState = PlayerState.newPlayer(tyrantId, "Tyrant");
TyrantStateManager manager = mock(TyrantStateManager.class);
when(manager.game()).thenReturn(game);
when(manager.player(tyrantId, "Tyrant")).thenReturn(tyrantState);
when(manager.players()).thenReturn(Map.of(tyrantId, tyrantState));
Player tyrant = mock(Player.class);
when(tyrant.getUniqueId()).thenReturn(tyrantId);
when(tyrant.getName()).thenReturn("Tyrant");
TyrantControlPanelRenderer renderer = mock(TyrantControlPanelRenderer.class);
DefaultTyrantControlPanel panel = new DefaultTyrantControlPanel(
manager, renderer,
Clock.fixed(Instant.parse("2026-08-21T12:00:00Z"), ZoneOffset.UTC)
);
panel.open(tyrant);
verify(renderer).openOverview(org.mockito.ArgumentMatchers.eq(tyrant), any());
}
}
@@ -20,6 +20,29 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
final class TyrantCommandTest {
@Test
void activeTyrantCanOpenControlPanelWithNoArgumentsOrMenuSubcommand() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
GameState game = new GameState(
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
0, 1, Set.of()
);
TyrantStateManager manager = mock(TyrantStateManager.class);
when(manager.game()).thenReturn(game);
Player tyrant = mock(Player.class);
when(tyrant.getUniqueId()).thenReturn(tyrantId);
TyrantControlPanel controlPanel = mock(TyrantControlPanel.class);
TyrantCommand command = new TyrantCommand(
manager, new TyrantProgressionService(), controlPanel
);
command.onCommand(tyrant, mock(Command.class), "tyrant", new String[0]);
command.onCommand(tyrant, mock(Command.class), "tyrant", new String[] {"menu"});
verify(controlPanel, org.mockito.Mockito.times(2)).open(tyrant);
}
@Test
void choicesClearlyIdentifyPurchasedAvailableAndUnavailableUnlocks() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
@@ -0,0 +1,105 @@
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 TyrantControlPanelJoinListenerTest {
@Test
void delayedTaskDoesNotOpenAfterEventPauses() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
GameState paused = new GameState(
GameLifecycle.PAUSED, Optional.of(tyrantId), Optional.empty(),
Optional.empty(), Optional.empty(), Optional.of(java.time.Instant.EPOCH),
Duration.ZERO, 0, 1, Set.of()
);
TyrantStateManager manager = mock(TyrantStateManager.class);
when(manager.game()).thenReturn(paused);
Player tyrant = mock(Player.class);
when(tyrant.getUniqueId()).thenReturn(tyrantId);
when(tyrant.isOnline()).thenReturn(true);
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
when(event.getPlayer()).thenReturn(tyrant);
DelayedTaskScheduler scheduler = mock(DelayedTaskScheduler.class);
TyrantControlPanel panel = mock(TyrantControlPanel.class);
TyrantControlPanelJoinListener listener = new TyrantControlPanelJoinListener(
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(tyrant);
}
@Test
void delayedTaskDoesNotReopenPanelOpenedManually() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
GameState game = new GameState(
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
0, 1, Set.of()
);
TyrantStateManager manager = mock(TyrantStateManager.class);
when(manager.game()).thenReturn(game);
Player tyrant = mock(Player.class);
when(tyrant.getUniqueId()).thenReturn(tyrantId);
when(tyrant.isOnline()).thenReturn(true);
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
when(event.getPlayer()).thenReturn(tyrant);
DelayedTaskScheduler scheduler = mock(DelayedTaskScheduler.class);
TyrantControlPanel panel = mock(TyrantControlPanel.class);
when(panel.openedThisSession(tyrantId)).thenReturn(true);
TyrantControlPanelJoinListener listener = new TyrantControlPanelJoinListener(
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(tyrant);
}
@Test
void opensPanelOneHundredTicksAfterActiveTyrantJoins() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
GameState game = new GameState(
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
0, 1, Set.of()
);
TyrantStateManager manager = mock(TyrantStateManager.class);
when(manager.game()).thenReturn(game);
Player tyrant = mock(Player.class);
when(tyrant.getUniqueId()).thenReturn(tyrantId);
when(tyrant.isOnline()).thenReturn(true);
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
when(event.getPlayer()).thenReturn(tyrant);
DelayedTaskScheduler scheduler = mock(DelayedTaskScheduler.class);
TyrantControlPanel panel = mock(TyrantControlPanel.class);
TyrantControlPanelJoinListener listener = new TyrantControlPanelJoinListener(
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(tyrant);
}
}
@@ -0,0 +1,49 @@
package games.dmg.spigottyrant;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
final class TyrantControlPanelModelTest {
@Test
void overviewShowsProgressPurchasesAssignmentsAndIntelligenceCooldown() {
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
UUID assassinId = UUID.fromString("22222222-2222-2222-2222-222222222222");
Instant now = Instant.parse("2026-08-21T12:00:00Z");
GameState game = new GameState(
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
3, 2, Set.of(TyrantUnlock.ASSASSIN, TyrantUnlock.ROSTER_INTELLIGENCE)
);
PlayerState tyrant = new PlayerState(
tyrantId, "Tyrant", Optional.of(now), Optional.empty(), TyrantClass.NONE,
Optional.empty(), Map.of(Ability.ROSTER_INTELLIGENCE, now.plusSeconds(90)),
Set.of(), java.util.List.of()
);
PlayerState assassin = new PlayerState(
assassinId, "Blade", Optional.of(now), Optional.empty(), TyrantClass.ASSASSIN,
Optional.empty(), Map.of(), Set.of(), java.util.List.of()
);
TyrantControlPanelModel model = TyrantControlPanelModel.create(
game, tyrant, Map.of(tyrantId, tyrant, assassinId, assassin), now
);
assertEquals(3, model.level());
assertEquals(2, model.unspentChoices());
assertEquals(UnlockAvailability.PURCHASED,
model.unlocks().get(TyrantUnlock.ASSASSIN));
assertEquals(
Set.of(TyrantUnlock.ASSASSIN, TyrantUnlock.ROSTER_INTELLIGENCE),
model.purchasedUnlocks()
);
assertEquals("Blade", model.classHolders().get(TyrantClass.ASSASSIN));
assertEquals(Duration.ofSeconds(90), model.intelligenceCooldown());
}
}