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,434 @@
package games.dmg.spigottyrant;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
public final class BukkitTyrantControlPanelRenderer
implements TyrantControlPanelRenderer, Listener {
private static final String TITLE = ChatColor.DARK_RED + "Tyrant Control Panel";
private final Server server;
private final TyrantStateManager stateManager;
private final Clock clock;
public BukkitTyrantControlPanelRenderer(
Server server,
TyrantStateManager stateManager,
Clock clock
) {
this.server = server;
this.stateManager = stateManager;
this.clock = clock;
}
@Override
public void openOverview(Player player, TyrantControlPanelModel model) {
MenuHolder holder = new MenuHolder(MenuView.OVERVIEW, null, null);
Inventory inventory = create(holder, 27, TITLE);
inventory.setItem(10, item(Material.NETHER_STAR, ChatColor.GOLD + "Reign Overview", List.of(
"Level: " + model.level(),
"Unspent choices: " + model.unspentChoices(),
"Purchased: " + (model.purchasedUnlocks().isEmpty()
? "none" : model.purchasedUnlocks())
)));
inventory.setItem(12, item(Material.ENCHANTED_BOOK, ChatColor.YELLOW + "Unlocks",
List.of("Review and purchase reign unlocks.")));
inventory.setItem(14, item(Material.ARMOR_STAND, ChatColor.AQUA + "Class Assignments",
classSummary(model)));
String intelligence = intelligenceText(model);
inventory.setItem(16, item(Material.SPYGLASS, ChatColor.LIGHT_PURPLE + "Roster Intelligence",
List.of(intelligence, "Click to activate when ready.")));
player.openInventory(inventory);
}
@EventHandler
public void onClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)
|| !(event.getView().getTopInventory().getHolder() instanceof MenuHolder holder)) {
return;
}
event.setCancelled(true);
if (event.getRawSlot() < 0
|| event.getRawSlot() >= event.getView().getTopInventory().getSize()) {
return;
}
if (!isActiveTyrant(player)) {
player.closeInventory();
player.sendMessage(ChatColor.RED + "This Tyrant control panel is no longer active.");
return;
}
switch (holder.view()) {
case OVERVIEW -> clickOverview(player, event.getRawSlot());
case UNLOCKS -> clickUnlocks(player, event.getRawSlot());
case PURCHASE_CONFIRMATION -> clickPurchaseConfirmation(
player, event.getRawSlot(), holder.unlock()
);
case CLASSES -> clickClasses(player, event.getRawSlot());
case ABILITIES -> clickAbilities(player, event.getRawSlot());
case PLAYERS -> clickPlayers(player, event.getRawSlot(), holder.tyrantClass());
case ASSIGN_CONFIRMATION -> clickAssignmentConfirmation(
player, event.getRawSlot(), holder.tyrantClass(), holder.targetId()
);
}
}
private void clickOverview(Player player, int slot) {
if (slot == 12) {
openUnlocks(player);
} else if (slot == 14) {
openClasses(player);
} else if (slot == 16) {
openAbilities(player);
}
}
private void openAbilities(Player player) {
TyrantControlPanelModel model = model(player);
MenuHolder holder = new MenuHolder(MenuView.ABILITIES, null, null);
Inventory inventory = create(holder, 27, TITLE + " — Abilities");
inventory.setItem(11, item(Material.BLAZE_POWDER, ChatColor.RED + "Permanent Strength",
List.of(model.purchasedUnlocks().contains(TyrantUnlock.STRENGTH)
? "Purchased and active." : "Not purchased.")));
inventory.setItem(13, item(Material.IRON_CHESTPLATE,
ChatColor.BLUE + "Permanent Resistance",
List.of(model.purchasedUnlocks().contains(TyrantUnlock.RESISTANCE)
? "Purchased and active." : "Not purchased.")));
inventory.setItem(15, item(Material.SPYGLASS, ChatColor.LIGHT_PURPLE
+ "Roster Intelligence", List.of(
intelligenceText(model), "Click to activate when ready."
)));
inventory.setItem(22, backItem());
player.openInventory(inventory);
}
private void clickAbilities(Player player, int slot) {
if (slot == 22) {
refresh(player);
} else if (slot == 15) {
player.performCommand("tyrant intelligence");
openAbilities(player);
}
}
private void openUnlocks(Player player) {
TyrantControlPanelModel model = model(player);
MenuHolder holder = new MenuHolder(MenuView.UNLOCKS, null, null);
Inventory inventory = create(holder, 27, TITLE + " — Unlocks");
int[] slots = {10, 11, 12, 14, 15, 16};
TyrantUnlock[] unlocks = TyrantUnlock.values();
for (int index = 0; index < unlocks.length; index++) {
TyrantUnlock unlock = unlocks[index];
UnlockAvailability availability = model.unlocks().get(unlock);
Material material = switch (availability) {
case PURCHASED -> Material.LIME_DYE;
case AVAILABLE -> Material.GOLD_INGOT;
case UNAVAILABLE -> Material.GRAY_DYE;
};
inventory.setItem(slots[index], item(material,
ChatColor.YELLOW + readable(unlock.name()),
List.of("Status: " + availability, "Cost: 1 choice")));
}
inventory.setItem(22, backItem());
player.openInventory(inventory);
}
private void clickUnlocks(Player player, int slot) {
if (slot == 22) {
refresh(player);
return;
}
int[] slots = {10, 11, 12, 14, 15, 16};
TyrantUnlock[] unlocks = TyrantUnlock.values();
for (int index = 0; index < slots.length; index++) {
if (slot == slots[index]
&& model(player).unlocks().get(unlocks[index])
== UnlockAvailability.AVAILABLE) {
openPurchaseConfirmation(player, unlocks[index]);
return;
}
}
}
private void openPurchaseConfirmation(Player player, TyrantUnlock unlock) {
MenuHolder holder = new MenuHolder(MenuView.PURCHASE_CONFIRMATION, unlock, null);
Inventory inventory = create(holder, 27, TITLE + " — Confirm Purchase");
inventory.setItem(11, item(Material.RED_WOOL, ChatColor.RED + "Cancel", List.of()));
inventory.setItem(13, item(Material.ENCHANTED_BOOK, ChatColor.YELLOW + readable(unlock.name()),
List.of("Cost: 1 choice")));
inventory.setItem(15, item(Material.LIME_WOOL, ChatColor.GREEN + "Confirm", List.of()));
player.openInventory(inventory);
}
private void clickPurchaseConfirmation(Player player, int slot, TyrantUnlock unlock) {
if (slot == 11) {
openUnlocks(player);
} else if (slot == 15 && unlock != null) {
player.performCommand("tyrant buy " + unlock.name().toLowerCase(Locale.ROOT));
openUnlocks(player);
}
}
private void openClasses(Player player) {
TyrantControlPanelModel model = model(player);
MenuHolder holder = new MenuHolder(MenuView.CLASSES, null, null);
Inventory inventory = create(holder, 27, TITLE + " — Classes");
TyrantClass[] classes = {TyrantClass.ASSASSIN, TyrantClass.FIXER, TyrantClass.TAMER};
int[] slots = {11, 13, 15};
for (int index = 0; index < classes.length; index++) {
TyrantClass tyrantClass = classes[index];
boolean purchased = stateManager.game().purchases().contains(unlockFor(tyrantClass));
inventory.setItem(slots[index], item(
purchased ? Material.ARMOR_STAND : Material.BARRIER,
ChatColor.AQUA + readable(tyrantClass.name()),
List.of("Holder: " + model.classHolders().get(tyrantClass),
purchased ? "Click to assign." : "Unlock not purchased.")));
}
inventory.setItem(22, backItem());
player.openInventory(inventory);
}
private void clickClasses(Player player, int slot) {
if (slot == 22) {
refresh(player);
return;
}
TyrantClass tyrantClass = switch (slot) {
case 11 -> TyrantClass.ASSASSIN;
case 13 -> TyrantClass.FIXER;
case 15 -> TyrantClass.TAMER;
default -> null;
};
if (tyrantClass != null
&& stateManager.game().purchases().contains(unlockFor(tyrantClass))) {
openPlayers(player, tyrantClass);
}
}
private void openPlayers(Player player, TyrantClass tyrantClass) {
MenuHolder holder = new MenuHolder(MenuView.PLAYERS, null, tyrantClass);
Inventory inventory = create(holder, 54, TITLE + " — Select Player");
int slot = 0;
for (Player candidate : server.getOnlinePlayers()) {
if (slot >= 45 || candidate.getUniqueId().equals(player.getUniqueId())) {
continue;
}
PlayerState state = stateManager.player(candidate.getUniqueId(), candidate.getName());
String eligibility = state.optedOutUntil().isPresent()
? ChatColor.RED + "Ineligible: opted out"
: state.tyrantClass() != TyrantClass.NONE
&& state.tyrantClass() != tyrantClass
? ChatColor.RED + "Ineligible: already " + state.tyrantClass()
: ChatColor.GREEN + "Eligible";
inventory.setItem(slot, item(Material.PLAYER_HEAD, candidate.getName(),
List.of(eligibility, candidate.getUniqueId().toString())));
slot++;
}
inventory.setItem(49, backItem());
player.openInventory(inventory);
}
private void clickPlayers(Player player, int slot, TyrantClass tyrantClass) {
if (slot == 49) {
openClasses(player);
return;
}
ItemStack clicked = player.getOpenInventory().getTopInventory().getItem(slot);
if (clicked == null || clicked.getType() != Material.PLAYER_HEAD) {
return;
}
List<String> lore = clicked.getItemMeta().getLore();
if (lore == null || lore.size() < 2 || !ChatColor.stripColor(lore.get(0)).equals("Eligible")) {
return;
}
try {
openAssignmentConfirmation(player, tyrantClass, UUID.fromString(lore.get(1)));
} catch (IllegalArgumentException ignored) {
player.sendMessage(ChatColor.RED + "That player selection is no longer valid.");
}
}
private void openAssignmentConfirmation(
Player player,
TyrantClass tyrantClass,
UUID targetId
) {
Player target = server.getPlayer(targetId);
if (target == null) {
player.sendMessage(ChatColor.RED + "That player is no longer online.");
openPlayers(player, tyrantClass);
return;
}
MenuHolder holder = new MenuHolder(
MenuView.ASSIGN_CONFIRMATION, null, tyrantClass, targetId
);
Inventory inventory = create(holder, 27, TITLE + " — Confirm Assignment");
String current = model(player).classHolders().get(tyrantClass);
inventory.setItem(11, item(Material.RED_WOOL, ChatColor.RED + "Cancel", List.of()));
inventory.setItem(13, item(Material.ARMOR_STAND,
ChatColor.YELLOW + target.getName(),
List.of("Class: " + tyrantClass, "Current holder: " + current)));
inventory.setItem(15, item(Material.LIME_WOOL, ChatColor.GREEN + "Confirm", List.of()));
player.openInventory(inventory);
}
private void clickAssignmentConfirmation(
Player player,
int slot,
TyrantClass tyrantClass,
UUID targetId
) {
if (slot == 11) {
openPlayers(player, tyrantClass);
} else if (slot == 15 && tyrantClass != null && targetId != null) {
Player target = server.getPlayer(targetId);
if (target == null) {
player.sendMessage(ChatColor.RED + "That player is no longer online.");
} else {
player.performCommand("tyrant assign "
+ tyrantClass.name().toLowerCase(Locale.ROOT) + " " + target.getName());
}
openClasses(player);
}
}
private boolean isActiveTyrant(Player player) {
GameState game = stateManager.game();
return game.lifecycle() == GameLifecycle.RUNNING
&& game.tyrantId().filter(player.getUniqueId()::equals).isPresent();
}
private TyrantControlPanelModel model(Player player) {
return TyrantControlPanelModel.create(
stateManager.game(),
stateManager.player(player.getUniqueId(), player.getName()),
stateManager.players(),
clock.instant()
);
}
private void refresh(Player player) {
openOverview(player, model(player));
}
private Inventory create(MenuHolder holder, int size, String title) {
Inventory inventory = server.createInventory(holder, size, title);
holder.inventory = inventory;
return inventory;
}
private static ItemStack item(Material material, String name, List<String> lore) {
ItemStack item = new ItemStack(material);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(name);
meta.setLore(lore.stream().map(line -> ChatColor.GRAY + line).toList());
item.setItemMeta(meta);
return item;
}
private static ItemStack backItem() {
return item(Material.ARROW, ChatColor.YELLOW + "Back", List.of());
}
private static List<String> classSummary(TyrantControlPanelModel model) {
List<String> summary = new ArrayList<>();
for (TyrantClass tyrantClass : new TyrantClass[] {
TyrantClass.ASSASSIN, TyrantClass.FIXER, TyrantClass.TAMER
}) {
summary.add(readable(tyrantClass.name()) + ": "
+ model.classHolders().get(tyrantClass));
}
return summary;
}
private static String intelligenceText(TyrantControlPanelModel model) {
if (model.unlocks().get(TyrantUnlock.ROSTER_INTELLIGENCE)
!= UnlockAvailability.PURCHASED) {
return "Unavailable: unlock not purchased.";
}
Duration cooldown = model.intelligenceCooldown();
return cooldown.isZero() ? "Ready" : "Cooldown: " + cooldown.toSeconds() + "s";
}
private static TyrantUnlock unlockFor(TyrantClass tyrantClass) {
return switch (tyrantClass) {
case ASSASSIN -> TyrantUnlock.ASSASSIN;
case FIXER -> TyrantUnlock.FIXER;
case TAMER -> TyrantUnlock.TAMER;
case NONE -> throw new IllegalArgumentException("NONE is not assignable");
};
}
private static String readable(String value) {
String lower = value.toLowerCase(Locale.ROOT).replace('_', ' ');
return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
}
private enum MenuView {
OVERVIEW,
UNLOCKS,
PURCHASE_CONFIRMATION,
CLASSES,
ABILITIES,
PLAYERS,
ASSIGN_CONFIRMATION
}
private static final class MenuHolder implements InventoryHolder {
private final MenuView view;
private final TyrantUnlock unlock;
private final TyrantClass tyrantClass;
private final UUID targetId;
private Inventory inventory;
private MenuHolder(MenuView view, TyrantUnlock unlock, TyrantClass tyrantClass) {
this(view, unlock, tyrantClass, null);
}
private MenuHolder(
MenuView view,
TyrantUnlock unlock,
TyrantClass tyrantClass,
UUID targetId
) {
this.view = view;
this.unlock = unlock;
this.tyrantClass = tyrantClass;
this.targetId = targetId;
}
private MenuView view() {
return view;
}
private TyrantUnlock unlock() {
return unlock;
}
private TyrantClass tyrantClass() {
return tyrantClass;
}
private UUID targetId() {
return targetId;
}
@Override
public Inventory getInventory() {
return inventory;
}
}
}
@@ -0,0 +1,57 @@
package games.dmg.spigottyrant;
import java.time.Clock;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
public final class DefaultTyrantControlPanel implements TyrantControlPanel {
private final TyrantStateManager stateManager;
private final TyrantControlPanelRenderer renderer;
private final Clock clock;
private final Set<UUID> openedSessions = new HashSet<>();
public DefaultTyrantControlPanel(
TyrantStateManager stateManager,
TyrantControlPanelRenderer renderer,
Clock clock
) {
this.stateManager = stateManager;
this.renderer = renderer;
this.clock = clock;
}
@Override
public void open(Player player) {
GameState game = stateManager.game();
if (game.lifecycle() != GameLifecycle.RUNNING
|| game.tyrantId().filter(player.getUniqueId()::equals).isEmpty()) {
player.sendMessage(ChatColor.RED
+ "Only the active Tyrant can open the control panel while the event is running.");
return;
}
PlayerState tyrant = stateManager.player(player.getUniqueId(), player.getName());
renderer.openOverview(
player,
TyrantControlPanelModel.create(game, tyrant, stateManager.players(), clock.instant())
);
openedSessions.add(player.getUniqueId());
}
@Override
public void beginSession(UUID playerId) {
openedSessions.remove(playerId);
}
@Override
public void endSession(UUID playerId) {
openedSessions.remove(playerId);
}
@Override
public boolean openedThisSession(UUID playerId) {
return openedSessions.contains(playerId);
}
}
@@ -0,0 +1,6 @@
package games.dmg.spigottyrant;
@FunctionalInterface
public interface DelayedTaskScheduler {
void schedule(Runnable task, long delayTicks);
}
@@ -80,6 +80,11 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
stateManager, tyrantPresentation
);
managedEffects.restoreAll();
BukkitTyrantControlPanelRenderer controlPanelRenderer =
new BukkitTyrantControlPanelRenderer(getServer(), stateManager, clock);
TyrantControlPanel controlPanel = new DefaultTyrantControlPanel(
stateManager, controlPanelRenderer, clock
);
Objects.requireNonNull(getCommand("tyrant"), "Missing tyrant command metadata")
.setExecutor(new TyrantCommand(
stateManager,
@@ -91,7 +96,8 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
new RoleRelinquishmentService(succession, maintenance),
tyrantPresentation,
abilityItems,
new TyrantAbilityService(settings.rosterIntelligenceCooldown())
new TyrantAbilityService(settings.rosterIntelligenceCooldown()),
controlPanel
));
Objects.requireNonNull(getCommand("vigilante"), "Missing vigilante metadata")
.setExecutor(new VigilanteCommand(stateManager, followers, onlinePlayers));
@@ -111,6 +117,16 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
new PlayerJoinListener(stateManager, clock, tyrantPresentation),
this
);
getServer().getPluginManager().registerEvents(controlPanelRenderer, this);
getServer().getPluginManager().registerEvents(
new TyrantControlPanelJoinListener(
stateManager,
controlPanel,
(task, delayTicks) -> getServer().getScheduler()
.runTaskLater(this, task, delayTicks)
),
this
);
getServer().getPluginManager().registerEvents(
new TyrantDeathListener(
stateManager, succession, tyrantPresentation, getServer(), clock
@@ -20,6 +20,7 @@ public final class TyrantCommand implements CommandExecutor {
private final TyrantPresentation presentation;
private final AbilityItemService abilityItems;
private final TyrantAbilityService tyrantAbilities;
private final TyrantControlPanel controlPanel;
public TyrantCommand(
TyrantStateManager stateManager,
@@ -34,6 +35,19 @@ public final class TyrantCommand implements CommandExecutor {
);
}
public TyrantCommand(
TyrantStateManager stateManager,
TyrantProgressionService progression,
TyrantControlPanel controlPanel
) {
this(
stateManager, progression, new ClassAssignmentService(),
new BukkitOnlinePlayerDirectory(), Clock.systemUTC(),
new ParticipationService(Duration.ofDays(7)), null, null, null,
new TyrantAbilityService(Duration.ofHours(24)), controlPanel
);
}
TyrantCommand(
TyrantStateManager stateManager,
TyrantProgressionService progression,
@@ -44,7 +58,7 @@ public final class TyrantCommand implements CommandExecutor {
this(
stateManager, progression, assignments, onlinePlayers, clock,
new ParticipationService(Duration.ofDays(7)), null, null, null,
new TyrantAbilityService(Duration.ofHours(24))
new TyrantAbilityService(Duration.ofHours(24)), TyrantCommand::unavailablePanel
);
}
@@ -59,6 +73,26 @@ public final class TyrantCommand implements CommandExecutor {
TyrantPresentation presentation,
AbilityItemService abilityItems,
TyrantAbilityService tyrantAbilities
) {
this(
stateManager, progression, assignments, onlinePlayers, clock, participation,
relinquishment, presentation, abilityItems, tyrantAbilities,
TyrantCommand::unavailablePanel
);
}
public TyrantCommand(
TyrantStateManager stateManager,
TyrantProgressionService progression,
ClassAssignmentService assignments,
OnlinePlayerDirectory onlinePlayers,
Clock clock,
ParticipationService participation,
RoleRelinquishmentService relinquishment,
TyrantPresentation presentation,
AbilityItemService abilityItems,
TyrantAbilityService tyrantAbilities,
TyrantControlPanel controlPanel
) {
this.stateManager = stateManager;
this.progression = progression;
@@ -70,6 +104,7 @@ public final class TyrantCommand implements CommandExecutor {
this.presentation = presentation;
this.abilityItems = abilityItems;
this.tyrantAbilities = tyrantAbilities;
this.controlPanel = controlPanel;
}
@Override
@@ -83,7 +118,12 @@ public final class TyrantCommand implements CommandExecutor {
sender.sendMessage("This command must be used by a player.");
return true;
}
if (arguments.length == 0 || arguments[0].equalsIgnoreCase("choices")) {
if (arguments.length == 0
|| arguments.length == 1 && arguments[0].equalsIgnoreCase("menu")) {
controlPanel.open(player);
return true;
}
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("choices")) {
showChoices(player);
return true;
}
@@ -121,7 +161,7 @@ public final class TyrantCommand implements CommandExecutor {
}
player.sendMessage(
ChatColor.YELLOW
+ "Usage: /tyrant <status|choices|buy|assign|item|intelligence|optout|optin|"
+ "Usage: /tyrant <menu|status|choices|buy|assign|item|intelligence|optout|optin|"
+ "relinquish confirm>"
);
return true;
@@ -303,6 +343,10 @@ public final class TyrantCommand implements CommandExecutor {
+ readable(result.status()) + ".");
}
private static void unavailablePanel(Player player) {
player.sendMessage(ChatColor.RED + "The Tyrant control panel is unavailable.");
}
private static String readable(Enum<?> status) {
return status.name().toLowerCase(Locale.ROOT).replace('_', ' ');
}
@@ -0,0 +1,19 @@
package games.dmg.spigottyrant;
import java.util.UUID;
import org.bukkit.entity.Player;
@FunctionalInterface
public interface TyrantControlPanel {
void open(Player player);
default void beginSession(UUID playerId) {
}
default void endSession(UUID playerId) {
}
default boolean openedThisSession(UUID playerId) {
return false;
}
}
@@ -0,0 +1,48 @@
package games.dmg.spigottyrant;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public final class TyrantControlPanelJoinListener implements Listener {
private static final long OPEN_DELAY_TICKS = 100L;
private final TyrantStateManager stateManager;
private final TyrantControlPanel controlPanel;
private final DelayedTaskScheduler scheduler;
public TyrantControlPanelJoinListener(
TyrantStateManager stateManager,
TyrantControlPanel controlPanel,
DelayedTaskScheduler scheduler
) {
this.stateManager = stateManager;
this.controlPanel = controlPanel;
this.scheduler = scheduler;
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
controlPanel.beginSession(playerId);
scheduler.schedule(() -> openIfStillTyrant(player), OPEN_DELAY_TICKS);
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
controlPanel.endSession(event.getPlayer().getUniqueId());
}
private void openIfStillTyrant(Player player) {
GameState game = stateManager.game();
if (player.isOnline()
&& game.lifecycle() == GameLifecycle.RUNNING
&& game.tyrantId().filter(player.getUniqueId()::equals).isPresent()
&& !controlPanel.openedThisSession(player.getUniqueId())) {
controlPanel.open(player);
}
}
}
@@ -0,0 +1,67 @@
package games.dmg.spigottyrant;
import java.time.Duration;
import java.time.Instant;
import java.util.EnumMap;
import java.util.Map;
import java.util.Set;
public record TyrantControlPanelModel(
int level,
int unspentChoices,
Map<TyrantUnlock, UnlockAvailability> unlocks,
Map<TyrantClass, String> classHolders,
Duration intelligenceCooldown
) {
public TyrantControlPanelModel {
unlocks = Map.copyOf(unlocks);
classHolders = Map.copyOf(classHolders);
}
public Set<TyrantUnlock> purchasedUnlocks() {
return unlocks.entrySet().stream()
.filter(entry -> entry.getValue() == UnlockAvailability.PURCHASED)
.map(Map.Entry::getKey)
.collect(java.util.stream.Collectors.toUnmodifiableSet());
}
public static TyrantControlPanelModel create(
GameState game,
PlayerState tyrant,
Map<java.util.UUID, PlayerState> players,
Instant now
) {
Map<TyrantUnlock, UnlockAvailability> unlocks = new EnumMap<>(TyrantUnlock.class);
for (TyrantUnlock unlock : TyrantUnlock.values()) {
UnlockAvailability availability;
if (game.purchases().contains(unlock)) {
availability = UnlockAvailability.PURCHASED;
} else if (game.lifecycle() == GameLifecycle.RUNNING
&& game.unspentChoices() > 0) {
availability = UnlockAvailability.AVAILABLE;
} else {
availability = UnlockAvailability.UNAVAILABLE;
}
unlocks.put(unlock, availability);
}
Map<TyrantClass, String> holders = new EnumMap<>(TyrantClass.class);
for (TyrantClass tyrantClass : new TyrantClass[] {
TyrantClass.ASSASSIN, TyrantClass.FIXER, TyrantClass.TAMER
}) {
String holder = players.values().stream()
.filter(player -> player.tyrantClass() == tyrantClass)
.map(PlayerState::latestName)
.findFirst()
.orElse("Unassigned");
holders.put(tyrantClass, holder);
}
Duration cooldown = tyrant.cooldownEnds().getOrDefault(
Ability.ROSTER_INTELLIGENCE, now
).isAfter(now)
? Duration.between(now, tyrant.cooldownEnds().get(Ability.ROSTER_INTELLIGENCE))
: Duration.ZERO;
return new TyrantControlPanelModel(
game.tyrantLevel(), game.unspentChoices(), unlocks, holders, cooldown
);
}
}
@@ -0,0 +1,8 @@
package games.dmg.spigottyrant;
import org.bukkit.entity.Player;
@FunctionalInterface
public interface TyrantControlPanelRenderer {
void openOverview(Player player, TyrantControlPanelModel model);
}
@@ -0,0 +1,7 @@
package games.dmg.spigottyrant;
public enum UnlockAvailability {
PURCHASED,
AVAILABLE,
UNAVAILABLE
}
+1 -1
View File
@@ -7,7 +7,7 @@ author: dmg.games
commands:
tyrant:
description: View and use Spigot Tyrant game features.
usage: /tyrant <status|choices|buy|assign|item|intelligence|optout|optin|relinquish confirm>
usage: /tyrant <menu|status|choices|buy|assign|item|intelligence|optout|optin|relinquish confirm>
vigilante:
description: Manage Vigilante Followers.
usage: /vigilante <invite <player>|accept|dismiss <player>|leave>