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,38 @@
package games.dmg.spigottyrant;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Server;
import org.bukkit.entity.Player;
public final class BukkitFollowerPresenceProvider implements FollowerPresenceProvider {
private final Server server;
private final double rangeSquared;
public BukkitFollowerPresenceProvider(Server server, double rangeBlocks) {
this.server = server;
this.rangeSquared = rangeBlocks * rangeBlocks;
}
@Override
public Map<UUID, FollowerPresence> inspect(
Player vigilante,
Map<UUID, PlayerState> players
) {
Map<UUID, FollowerPresence> result = new HashMap<>();
for (PlayerState state : players.values()) {
Player follower = server.getPlayer(state.playerId());
boolean online = follower != null && follower.isOnline();
boolean living = online && !follower.isDead();
boolean sameWorld = living && follower.getWorld().equals(vigilante.getWorld());
boolean withinRange = sameWorld && follower.getLocation()
.distanceSquared(vigilante.getLocation()) <= rangeSquared;
result.put(
state.playerId(),
new FollowerPresence(online, living, sameWorld, withinRange)
);
}
return Map.copyOf(result);
}
}
@@ -255,7 +255,9 @@ public final class BukkitTyrantControlPanelRenderer
return;
}
try {
openAssignmentConfirmation(player, tyrantClass, UUID.fromString(lore.get(1)));
openAssignmentConfirmation(
player, tyrantClass, UUID.fromString(ChatColor.stripColor(lore.get(1)))
);
} catch (IllegalArgumentException ignored) {
player.sendMessage(ChatColor.RED + "That player selection is no longer valid.");
}
@@ -0,0 +1,409 @@
package games.dmg.spigottyrant;
import java.util.List;
import java.util.Map;
import java.util.Set;
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 BukkitVigilanteControlPanelRenderer
implements VigilanteControlPanelRenderer, Listener {
private static final String TITLE = ChatColor.DARK_BLUE + "Vigilante Control Panel";
private final Server server;
private final TyrantStateManager stateManager;
private final FollowerService followers;
private final FollowerPresenceProvider presenceProvider;
private final int strengthCap;
private final int resistanceCap;
public BukkitVigilanteControlPanelRenderer(
Server server,
TyrantStateManager stateManager,
FollowerService followers,
FollowerPresenceProvider presenceProvider,
int strengthCap,
int resistanceCap
) {
this.server = server;
this.stateManager = stateManager;
this.followers = followers;
this.presenceProvider = presenceProvider;
this.strengthCap = strengthCap;
this.resistanceCap = resistanceCap;
}
@Override
public void openOverview(Player player, VigilanteControlPanelModel model) {
MenuHolder holder = new MenuHolder(MenuView.OVERVIEW, null);
Inventory inventory = create(holder, 27, TITLE);
inventory.setItem(10, item(Material.SHIELD, ChatColor.BLUE + "Resistance Overview",
List.of(
"Followers: " + model.followers().size(),
"Pending invitations: " + model.pendingInvitations().size(),
"Nearby contributors: " + model.nearbyEligibleFollowers(),
"Strength: " + model.strengthLevel() + "/" + model.strengthCap(),
"Resistance: " + model.resistanceLevel() + "/" + model.resistanceCap()
)));
inventory.setItem(12, item(Material.WRITABLE_BOOK, ChatColor.GREEN + "Recruitment",
List.of("Invite eligible online players.")));
inventory.setItem(14, item(Material.PLAYER_HEAD, ChatColor.AQUA + "Manage Followers",
followerSummary(model)));
inventory.setItem(16, item(Material.REDSTONE_TORCH, ChatColor.RED + "Role Control",
List.of("Relinquish the Vigilante role.")));
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 (!isActiveVigilante(player)) {
player.closeInventory();
player.sendMessage(ChatColor.RED
+ "This Vigilante control panel is no longer active.");
return;
}
switch (holder.view()) {
case OVERVIEW -> clickOverview(player, event.getRawSlot());
case RECRUITMENT -> clickRecruitment(player, event.getRawSlot());
case INVITE_CONFIRMATION -> clickInviteConfirmation(
player, event.getRawSlot(), holder.targetId()
);
case FOLLOWERS -> clickFollowers(player, event.getRawSlot());
case DISMISS_CONFIRMATION -> clickDismissConfirmation(
player, event.getRawSlot(), holder.targetId()
);
case ROLE -> clickRole(player, event.getRawSlot());
case RELINQUISH_CONFIRMATION -> clickRelinquishConfirmation(
player, event.getRawSlot()
);
}
}
private void clickOverview(Player player, int slot) {
if (slot == 12) {
openRecruitment(player);
} else if (slot == 14) {
openFollowers(player);
} else if (slot == 16) {
openRole(player);
}
}
private void openRecruitment(Player player) {
MenuHolder holder = new MenuHolder(MenuView.RECRUITMENT, null);
Inventory inventory = create(holder, 54, TITLE + " — Recruitment");
Set<UUID> pending = followers.invitedPlayerIds(player.getUniqueId());
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 = recruitmentEligibility(candidate.getUniqueId(), state, pending);
inventory.setItem(slot, playerItem(candidate.getName(), candidate.getUniqueId(),
eligibility));
slot++;
}
inventory.setItem(49, backItem());
player.openInventory(inventory);
}
private String recruitmentEligibility(
UUID candidateId,
PlayerState state,
Set<UUID> pending
) {
GameState game = stateManager.game();
if (pending.contains(candidateId)) {
return ChatColor.YELLOW + "Pending invitation";
}
if (state.optedOutUntil().isPresent()) {
return ChatColor.RED + "Ineligible: opted out";
}
if (game.tyrantId().filter(candidateId::equals).isPresent()) {
return ChatColor.RED + "Ineligible: Tyrant";
}
if (game.vigilanteId().filter(candidateId::equals).isPresent()) {
return ChatColor.RED + "Ineligible: Vigilante";
}
if (state.followerOf().isPresent()) {
return ChatColor.RED + "Ineligible: already a Follower";
}
return ChatColor.GREEN + "Eligible";
}
private void clickRecruitment(Player player, int slot) {
if (slot == 49) {
refresh(player);
return;
}
selectedPlayer(player, slot, "Eligible")
.ifPresent(targetId -> openInviteConfirmation(player, targetId));
}
private void openInviteConfirmation(Player player, UUID targetId) {
Player target = server.getPlayer(targetId);
if (target == null) {
player.sendMessage(ChatColor.RED + "That player is no longer online.");
openRecruitment(player);
return;
}
MenuHolder holder = new MenuHolder(MenuView.INVITE_CONFIRMATION, targetId);
Inventory inventory = create(holder, 27, TITLE + " — Confirm Invitation");
inventory.setItem(11, cancelItem());
inventory.setItem(13, item(Material.WRITABLE_BOOK, ChatColor.YELLOW + target.getName(),
List.of("Invite this player to become a Follower?")));
inventory.setItem(15, confirmItem());
player.openInventory(inventory);
}
private void clickInviteConfirmation(Player player, int slot, UUID targetId) {
if (slot == 11) {
openRecruitment(player);
} else if (slot == 15 && targetId != null) {
Player target = server.getPlayer(targetId);
if (target == null) {
player.sendMessage(ChatColor.RED + "That player is no longer online.");
} else {
player.performCommand("vigilante invite " + target.getName());
}
openRecruitment(player);
}
}
private void openFollowers(Player player) {
VigilanteControlPanelModel model = model(player);
MenuHolder holder = new MenuHolder(MenuView.FOLLOWERS, null);
Inventory inventory = create(holder, 54, TITLE + " — Followers");
int slot = 0;
for (VigilanteFollowerView follower : model.followers()) {
if (slot >= 45) {
break;
}
FollowerPresence presence = follower.presence();
List<String> status = List.of(
presence.online() ? "Online" : "Offline",
presence.living() ? "Living" : "Dead or unavailable",
presence.sameWorld() ? "Same world" : "Different world or unavailable",
presence.withinRange() ? "Within range" : "Outside range or unavailable",
follower.optedOut() ? "Opted out" : "Participating",
presence.online() ? ChatColor.GREEN + "Click to dismiss."
: ChatColor.RED + "Must be online to dismiss."
);
inventory.setItem(slot, playerItem(
follower.name(), follower.playerId(), status.toString()
));
slot++;
}
inventory.setItem(49, backItem());
player.openInventory(inventory);
}
private void clickFollowers(Player player, int slot) {
if (slot == 49) {
refresh(player);
return;
}
selectedPlayer(player, slot, "Online")
.filter(targetId -> server.getPlayer(targetId) != null)
.ifPresent(targetId -> openDismissConfirmation(player, targetId));
}
private void openDismissConfirmation(Player player, UUID targetId) {
Player target = server.getPlayer(targetId);
if (target == null) {
player.sendMessage(ChatColor.RED + "That Follower is no longer online.");
openFollowers(player);
return;
}
MenuHolder holder = new MenuHolder(MenuView.DISMISS_CONFIRMATION, targetId);
Inventory inventory = create(holder, 27, TITLE + " — Confirm Dismissal");
inventory.setItem(11, cancelItem());
inventory.setItem(13, item(Material.PLAYER_HEAD, ChatColor.YELLOW + target.getName(),
List.of("Dismiss this Follower?")));
inventory.setItem(15, confirmItem());
player.openInventory(inventory);
}
private void clickDismissConfirmation(Player player, int slot, UUID targetId) {
if (slot == 11) {
openFollowers(player);
} else if (slot == 15 && targetId != null) {
Player target = server.getPlayer(targetId);
if (target == null) {
player.sendMessage(ChatColor.RED + "That Follower is no longer online.");
} else {
player.performCommand("vigilante dismiss " + target.getName());
}
openFollowers(player);
}
}
private void openRole(Player player) {
MenuHolder holder = new MenuHolder(MenuView.ROLE, null);
Inventory inventory = create(holder, 27, TITLE + " — Role Control");
inventory.setItem(13, item(Material.REDSTONE_TORCH,
ChatColor.RED + "Relinquish Vigilante Role",
List.of("Followers will be cleared and a replacement selected.",
"Click to review confirmation.")));
inventory.setItem(22, backItem());
player.openInventory(inventory);
}
private void clickRole(Player player, int slot) {
if (slot == 22) {
refresh(player);
} else if (slot == 13) {
MenuHolder holder = new MenuHolder(MenuView.RELINQUISH_CONFIRMATION, null);
Inventory inventory = create(holder, 27, TITLE + " — Confirm Relinquishment");
inventory.setItem(11, cancelItem());
inventory.setItem(13, item(Material.BARRIER, ChatColor.RED + "Relinquish Role",
List.of("This clears all Followers and rerolls the Vigilante.")));
inventory.setItem(15, confirmItem());
player.openInventory(inventory);
}
}
private void clickRelinquishConfirmation(Player player, int slot) {
if (slot == 11) {
openRole(player);
} else if (slot == 15) {
player.closeInventory();
player.performCommand("tyrant relinquish confirm");
}
}
private java.util.Optional<UUID> selectedPlayer(
Player player,
int slot,
String requiredStatus
) {
ItemStack clicked = player.getOpenInventory().getTopInventory().getItem(slot);
if (clicked == null || clicked.getType() != Material.PLAYER_HEAD) {
return java.util.Optional.empty();
}
List<String> lore = clicked.getItemMeta().getLore();
if (lore == null || lore.size() < 2
|| !ChatColor.stripColor(lore.get(0)).contains(requiredStatus)) {
return java.util.Optional.empty();
}
try {
return java.util.Optional.of(UUID.fromString(
ChatColor.stripColor(lore.get(lore.size() - 1))
));
} catch (IllegalArgumentException ignored) {
player.sendMessage(ChatColor.RED + "That player selection is no longer valid.");
return java.util.Optional.empty();
}
}
private boolean isActiveVigilante(Player player) {
GameState game = stateManager.game();
return game.lifecycle() == GameLifecycle.RUNNING
&& game.vigilanteId().filter(player.getUniqueId()::equals).isPresent();
}
private VigilanteControlPanelModel model(Player player) {
Map<UUID, PlayerState> players = stateManager.players();
return VigilanteControlPanelModel.create(
stateManager.game(), players, followers.invitedPlayerIds(player.getUniqueId()),
presenceProvider.inspect(player, players), strengthCap, resistanceCap
);
}
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 playerItem(String name, UUID playerId, String status) {
return item(Material.PLAYER_HEAD, name, List.of(status, playerId.toString()));
}
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 ItemStack cancelItem() {
return item(Material.RED_WOOL, ChatColor.RED + "Cancel", List.of());
}
private static ItemStack confirmItem() {
return item(Material.LIME_WOOL, ChatColor.GREEN + "Confirm", List.of());
}
private static List<String> followerSummary(VigilanteControlPanelModel model) {
if (model.followers().isEmpty()) {
return List.of("No Followers assigned.");
}
return model.followers().stream()
.map(follower -> follower.name() + (follower.contributes() ? " — contributing" : ""))
.toList();
}
private enum MenuView {
OVERVIEW,
RECRUITMENT,
INVITE_CONFIRMATION,
FOLLOWERS,
DISMISS_CONFIRMATION,
ROLE,
RELINQUISH_CONFIRMATION
}
private static final class MenuHolder implements InventoryHolder {
private final MenuView view;
private final UUID targetId;
private Inventory inventory;
private MenuHolder(MenuView view, UUID targetId) {
this.view = view;
this.targetId = targetId;
}
private MenuView view() {
return view;
}
private UUID targetId() {
return targetId;
}
@Override
public Inventory getInventory() {
return inventory;
}
}
}
@@ -0,0 +1,73 @@
package games.dmg.spigottyrant;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
public final class DefaultVigilanteControlPanel implements VigilanteControlPanel {
private final TyrantStateManager stateManager;
private final FollowerService followers;
private final VigilanteControlPanelRenderer renderer;
private final FollowerPresenceProvider presenceProvider;
private final int strengthCap;
private final int resistanceCap;
private final Set<UUID> openedSessions = new HashSet<>();
public DefaultVigilanteControlPanel(
TyrantStateManager stateManager,
FollowerService followers,
VigilanteControlPanelRenderer renderer,
FollowerPresenceProvider presenceProvider,
int strengthCap,
int resistanceCap
) {
this.stateManager = stateManager;
this.followers = followers;
this.renderer = renderer;
this.presenceProvider = presenceProvider;
this.strengthCap = strengthCap;
this.resistanceCap = resistanceCap;
}
@Override
public void open(Player player) {
GameState game = stateManager.game();
if (game.lifecycle() != GameLifecycle.RUNNING
|| game.vigilanteId().filter(player.getUniqueId()::equals).isEmpty()) {
player.sendMessage(ChatColor.RED
+ "Only the active Vigilante can open the control panel while the event is running.");
return;
}
Map<UUID, PlayerState> players = stateManager.players();
renderer.openOverview(
player,
VigilanteControlPanelModel.create(
game,
players,
followers.invitedPlayerIds(player.getUniqueId()),
presenceProvider.inspect(player, players),
strengthCap,
resistanceCap
)
);
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,12 @@
package games.dmg.spigottyrant;
public record FollowerPresence(
boolean online,
boolean living,
boolean sameWorld,
boolean withinRange
) {
public boolean contributes() {
return online && living && sameWorld && withinRange;
}
}
@@ -0,0 +1,10 @@
package games.dmg.spigottyrant;
import java.util.Map;
import java.util.UUID;
import org.bukkit.entity.Player;
@FunctionalInterface
public interface FollowerPresenceProvider {
Map<UUID, FollowerPresence> inspect(Player vigilante, Map<UUID, PlayerState> players);
}
@@ -22,10 +22,14 @@ public final class FollowerService {
}
PlayerState target = players.get(targetId);
if (target == null || target.optedOutUntil().isPresent()
|| target.followerOf().isPresent()
|| game.tyrantId().filter(targetId::equals).isPresent()
|| game.vigilanteId().filter(targetId::equals).isPresent()) {
return result(players, FollowerStatus.INELIGIBLE);
}
if (vigilanteId.equals(invitations.get(targetId))) {
return result(players, FollowerStatus.ALREADY_INVITED);
}
invitations.put(targetId, vigilanteId);
return result(players, FollowerStatus.INVITED);
}
@@ -77,6 +81,13 @@ public final class FollowerService {
return updated(players, withFollower(target, Optional.empty()), FollowerStatus.LEFT);
}
public java.util.Set<UUID> invitedPlayerIds(UUID vigilanteId) {
return invitations.entrySet().stream()
.filter(entry -> entry.getValue().equals(vigilanteId))
.map(Map.Entry::getKey)
.collect(java.util.stream.Collectors.toUnmodifiableSet());
}
public void clearInvitations() {
invitations.clear();
}
@@ -2,6 +2,7 @@ package games.dmg.spigottyrant;
public enum FollowerStatus {
INVITED,
ALREADY_INVITED,
JOINED,
DISMISSED,
LEFT,
@@ -85,6 +85,18 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
TyrantControlPanel controlPanel = new DefaultTyrantControlPanel(
stateManager, controlPanelRenderer, clock
);
FollowerPresenceProvider followerPresence = new BukkitFollowerPresenceProvider(
getServer(), settings.followerRangeBlocks()
);
BukkitVigilanteControlPanelRenderer vigilanteControlPanelRenderer =
new BukkitVigilanteControlPanelRenderer(
getServer(), stateManager, followers, followerPresence,
settings.followerStrengthCap(), settings.followerResistanceCap()
);
VigilanteControlPanel vigilanteControlPanel = new DefaultVigilanteControlPanel(
stateManager, followers, vigilanteControlPanelRenderer, followerPresence,
settings.followerStrengthCap(), settings.followerResistanceCap()
);
Objects.requireNonNull(getCommand("tyrant"), "Missing tyrant command metadata")
.setExecutor(new TyrantCommand(
stateManager,
@@ -100,7 +112,9 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
controlPanel
));
Objects.requireNonNull(getCommand("vigilante"), "Missing vigilante metadata")
.setExecutor(new VigilanteCommand(stateManager, followers, onlinePlayers));
.setExecutor(new VigilanteCommand(
stateManager, followers, onlinePlayers, vigilanteControlPanel
));
Objects.requireNonNull(getCommand("tyrantadmin"), "Missing tyrantadmin metadata")
.setExecutor(new TyrantAdminCommand(
stateManager,
@@ -118,6 +132,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
this
);
getServer().getPluginManager().registerEvents(controlPanelRenderer, this);
getServer().getPluginManager().registerEvents(vigilanteControlPanelRenderer, this);
getServer().getPluginManager().registerEvents(
new TyrantControlPanelJoinListener(
stateManager,
@@ -127,6 +142,15 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
),
this
);
getServer().getPluginManager().registerEvents(
new VigilanteControlPanelJoinListener(
stateManager,
vigilanteControlPanel,
(task, delayTicks) -> getServer().getScheduler()
.runTaskLater(this, task, delayTicks)
),
this
);
getServer().getPluginManager().registerEvents(
new TyrantDeathListener(
stateManager, succession, tyrantPresentation, getServer(), clock
@@ -11,15 +11,30 @@ public final class VigilanteCommand implements CommandExecutor {
private final TyrantStateManager stateManager;
private final FollowerService followers;
private final OnlinePlayerDirectory onlinePlayers;
private final VigilanteControlPanel controlPanel;
public VigilanteCommand(
TyrantStateManager stateManager,
FollowerService followers,
OnlinePlayerDirectory onlinePlayers
) {
this(
stateManager, followers, onlinePlayers,
player -> player.sendMessage(ChatColor.RED
+ "The Vigilante control panel is unavailable.")
);
}
public VigilanteCommand(
TyrantStateManager stateManager,
FollowerService followers,
OnlinePlayerDirectory onlinePlayers,
VigilanteControlPanel controlPanel
) {
this.stateManager = stateManager;
this.followers = followers;
this.onlinePlayers = onlinePlayers;
this.controlPanel = controlPanel;
}
@Override
@@ -33,6 +48,11 @@ public final class VigilanteCommand implements CommandExecutor {
sender.sendMessage("This command must be used by a player.");
return true;
}
if (arguments.length == 0
|| arguments.length == 1 && arguments[0].equalsIgnoreCase("menu")) {
controlPanel.open(player);
return true;
}
FollowerResult result;
Player target = null;
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("accept")) {
@@ -61,7 +81,7 @@ public final class VigilanteCommand implements CommandExecutor {
);
} else {
player.sendMessage(ChatColor.YELLOW
+ "Usage: /vigilante <invite <player>|accept|dismiss <player>|leave>");
+ "Usage: /vigilante <menu|invite <player>|accept|dismiss <player>|leave>");
return true;
}
if (result.status() == FollowerStatus.JOINED
@@ -0,0 +1,19 @@
package games.dmg.spigottyrant;
import java.util.UUID;
import org.bukkit.entity.Player;
@FunctionalInterface
public interface VigilanteControlPanel {
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 VigilanteControlPanelJoinListener implements Listener {
private static final long OPEN_DELAY_TICKS = 100L;
private final TyrantStateManager stateManager;
private final VigilanteControlPanel controlPanel;
private final DelayedTaskScheduler scheduler;
public VigilanteControlPanelJoinListener(
TyrantStateManager stateManager,
VigilanteControlPanel 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(() -> openIfStillVigilante(player), OPEN_DELAY_TICKS);
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
controlPanel.endSession(event.getPlayer().getUniqueId());
}
private void openIfStillVigilante(Player player) {
GameState game = stateManager.game();
if (player.isOnline()
&& game.lifecycle() == GameLifecycle.RUNNING
&& game.vigilanteId().filter(player.getUniqueId()::equals).isPresent()
&& !controlPanel.openedThisSession(player.getUniqueId())) {
controlPanel.open(player);
}
}
}
@@ -0,0 +1,59 @@
package games.dmg.spigottyrant;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public record VigilanteControlPanelModel(
List<VigilanteFollowerView> followers,
Set<String> pendingInvitations,
int nearbyEligibleFollowers,
int strengthLevel,
int resistanceLevel,
int strengthCap,
int resistanceCap
) {
public VigilanteControlPanelModel {
followers = List.copyOf(followers);
pendingInvitations = Set.copyOf(pendingInvitations);
}
public static VigilanteControlPanelModel create(
GameState game,
Map<UUID, PlayerState> players,
Set<UUID> pendingInvitationIds,
Map<UUID, FollowerPresence> presence,
int strengthCap,
int resistanceCap
) {
UUID vigilanteId = game.vigilanteId().orElseThrow();
List<VigilanteFollowerView> followers = players.values().stream()
.filter(player -> player.followerOf().filter(vigilanteId::equals).isPresent())
.map(player -> new VigilanteFollowerView(
player.playerId(), player.latestName(), player.optedOutUntil().isPresent(),
presence.getOrDefault(
player.playerId(), new FollowerPresence(false, false, false, false)
)
))
.sorted(Comparator.comparing(VigilanteFollowerView::name))
.toList();
Set<String> pending = pendingInvitationIds.stream()
.map(players::get)
.filter(java.util.Objects::nonNull)
.map(PlayerState::latestName)
.collect(Collectors.toUnmodifiableSet());
int nearby = (int) followers.stream()
.filter(VigilanteFollowerView::contributes)
.count();
VigilantePower.Levels levels = VigilantePower.forFollowers(
nearby, strengthCap, resistanceCap
);
return new VigilanteControlPanelModel(
followers, pending, nearby, levels.strength(), levels.resistance(),
strengthCap, resistanceCap
);
}
}
@@ -0,0 +1,8 @@
package games.dmg.spigottyrant;
import org.bukkit.entity.Player;
@FunctionalInterface
public interface VigilanteControlPanelRenderer {
void openOverview(Player player, VigilanteControlPanelModel model);
}
@@ -0,0 +1,14 @@
package games.dmg.spigottyrant;
import java.util.UUID;
public record VigilanteFollowerView(
UUID playerId,
String name,
boolean optedOut,
FollowerPresence presence
) {
public boolean contributes() {
return !optedOut && presence.contributes();
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ commands:
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>
usage: /vigilante <menu|invite <player>|accept|dismiss <player>|leave>
tyrantadmin:
description: Administer the Spigot Tyrant game.
usage: /tyrantadmin <status|start|pause|resume|reset confirm>