feat(vigilante): add arena boss succession
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
public final class ArenaGeometry {
|
||||
private ArenaGeometry() {
|
||||
}
|
||||
|
||||
public static boolean contains(
|
||||
ArenaLocation center,
|
||||
double radius,
|
||||
String worldName,
|
||||
double x,
|
||||
double y,
|
||||
double z
|
||||
) {
|
||||
if (!center.worldName().equals(worldName)) {
|
||||
return false;
|
||||
}
|
||||
double deltaX = x - center.x();
|
||||
double deltaZ = z - center.z();
|
||||
return deltaX * deltaX + deltaZ * deltaZ <= radius * radius;
|
||||
}
|
||||
|
||||
public static ArenaPoint outside(
|
||||
ArenaLocation center,
|
||||
double radius,
|
||||
double x,
|
||||
double y,
|
||||
double z
|
||||
) {
|
||||
double deltaX = x - center.x();
|
||||
double deltaZ = z - center.z();
|
||||
double length = Math.hypot(deltaX, deltaZ);
|
||||
if (length < 0.0001) {
|
||||
deltaX = 1.0;
|
||||
deltaZ = 0.0;
|
||||
length = 1.0;
|
||||
}
|
||||
double distance = radius + 1.0;
|
||||
return new ArenaPoint(
|
||||
center.x() + deltaX / length * distance,
|
||||
y,
|
||||
center.z() + deltaZ / length * distance
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
public record ArenaLocation(
|
||||
String worldName,
|
||||
double x,
|
||||
double y,
|
||||
double z,
|
||||
float yaw,
|
||||
float pitch
|
||||
) {
|
||||
public ArenaLocation {
|
||||
if (worldName == null || worldName.isBlank()) {
|
||||
throw new IllegalArgumentException("arena world must not be blank");
|
||||
}
|
||||
worldName = worldName.trim();
|
||||
if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(z)
|
||||
|| !Float.isFinite(yaw) || !Float.isFinite(pitch)) {
|
||||
throw new IllegalArgumentException("arena coordinates must be finite");
|
||||
}
|
||||
}
|
||||
|
||||
public String display() {
|
||||
return worldName + " at " + Math.round(x) + ", " + Math.round(y) + ", "
|
||||
+ Math.round(z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ArenaLocationStore {
|
||||
Optional<ArenaLocation> location();
|
||||
|
||||
void set(ArenaLocation location);
|
||||
|
||||
static ArenaLocationStore unavailable() {
|
||||
return new ArenaLocationStore() {
|
||||
@Override
|
||||
public Optional<ArenaLocation> location() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(ArenaLocation location) {
|
||||
throw new IllegalStateException("arena location storage is unavailable");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
public record ArenaPoint(double x, double y, double z) {
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class BukkitConfigArenaLocationStore implements ArenaLocationStore {
|
||||
private static final String PATH = "vigilante-arena-location";
|
||||
private final JavaPlugin plugin;
|
||||
|
||||
public BukkitConfigArenaLocationStore(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ArenaLocation> location() {
|
||||
FileConfiguration config = plugin.getConfig();
|
||||
if (!config.isConfigurationSection(PATH)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String world = config.getString(PATH + ".world");
|
||||
if (world == null) {
|
||||
throw new IllegalArgumentException(PATH + ".world must be configured");
|
||||
}
|
||||
return Optional.of(new ArenaLocation(
|
||||
world,
|
||||
finite(config, "x"),
|
||||
finite(config, "y"),
|
||||
finite(config, "z"),
|
||||
(float) finite(config, "yaw"),
|
||||
(float) finite(config, "pitch")
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(ArenaLocation location) {
|
||||
FileConfiguration config = plugin.getConfig();
|
||||
config.set(PATH + ".world", location.worldName());
|
||||
config.set(PATH + ".x", location.x());
|
||||
config.set(PATH + ".y", location.y());
|
||||
config.set(PATH + ".z", location.z());
|
||||
config.set(PATH + ".yaw", location.yaw());
|
||||
config.set(PATH + ".pitch", location.pitch());
|
||||
plugin.saveConfig();
|
||||
}
|
||||
|
||||
private double finite(FileConfiguration config, String key) {
|
||||
String path = PATH + "." + key;
|
||||
if (!config.isSet(path) || !(config.get(path) instanceof Number number)
|
||||
|| !Double.isFinite(number.doubleValue())) {
|
||||
throw new IllegalArgumentException(path + " must be finite and numeric");
|
||||
}
|
||||
return number.doubleValue();
|
||||
}
|
||||
}
|
||||
@@ -262,7 +262,7 @@ public final class BukkitVigilanteControlPanelRenderer
|
||||
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.",
|
||||
List.of("Followers will be cleared and the arena will open.",
|
||||
"Click to review confirmation.")));
|
||||
inventory.setItem(22, backItem());
|
||||
player.openInventory(inventory);
|
||||
@@ -276,7 +276,7 @@ public final class BukkitVigilanteControlPanelRenderer
|
||||
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.")));
|
||||
List.of("This clears all Followers and opens the arena challenge.")));
|
||||
inventory.setItem(15, confirmItem());
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ import org.bukkit.configuration.ConfigurationSection;
|
||||
public record PluginSettings(
|
||||
double tyrantRangeBlocks,
|
||||
double followerRangeBlocks,
|
||||
double vigilanteArenaRadiusBlocks,
|
||||
double vigilanteBossHealth,
|
||||
double vigilanteBossDamage,
|
||||
double vigilanteBossArmor,
|
||||
Duration optOutDuration,
|
||||
Duration roleInactivity,
|
||||
Duration candidateActivityWindow,
|
||||
@@ -48,6 +52,12 @@ public record PluginSettings(
|
||||
public PluginSettings {
|
||||
requirePositiveFinite(tyrantRangeBlocks, "tyrant-range-blocks");
|
||||
requirePositiveFinite(followerRangeBlocks, "follower-range-blocks");
|
||||
requireFiniteRange(
|
||||
vigilanteArenaRadiusBlocks, 1.0, 512.0, "vigilante-arena-radius-blocks"
|
||||
);
|
||||
requireFiniteRange(vigilanteBossHealth, 1.0, 1024.0, "vigilante-boss-health");
|
||||
requireFiniteRange(vigilanteBossDamage, 0.1, 2048.0, "vigilante-boss-damage");
|
||||
requireFiniteRange(vigilanteBossArmor, 0.0, 30.0, "vigilante-boss-armor");
|
||||
requirePositive(optOutDuration, "opt-out-seconds");
|
||||
requirePositive(roleInactivity, "role-inactivity-seconds");
|
||||
requirePositive(candidateActivityWindow, "candidate-activity-window-seconds");
|
||||
@@ -93,6 +103,10 @@ public record PluginSettings(
|
||||
return new PluginSettings(
|
||||
decimal(values, "tyrant-range-blocks", 50.0),
|
||||
decimal(values, "follower-range-blocks", 50.0),
|
||||
decimal(values, "vigilante-arena-radius-blocks", 10.0),
|
||||
decimal(values, "vigilante-boss-health", 80.0),
|
||||
decimal(values, "vigilante-boss-damage", 8.0),
|
||||
decimal(values, "vigilante-boss-armor", 10.0),
|
||||
duration(values, "opt-out-seconds", Duration.ofDays(7)),
|
||||
duration(values, "role-inactivity-seconds", Duration.ofHours(48)),
|
||||
duration(values, "candidate-activity-window-seconds", Duration.ofHours(24)),
|
||||
@@ -227,6 +241,19 @@ public record PluginSettings(
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireFiniteRange(
|
||||
double value,
|
||||
double minimum,
|
||||
double maximum,
|
||||
String key
|
||||
) {
|
||||
if (!Double.isFinite(value) || value < minimum || value > maximum) {
|
||||
throw new IllegalArgumentException(
|
||||
key + " must be between " + minimum + " and " + maximum
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void requirePositive(Duration value, String key) {
|
||||
Objects.requireNonNull(value, key);
|
||||
if (value.isZero() || value.isNegative()) {
|
||||
|
||||
@@ -53,17 +53,9 @@ public final class RoleMaintenanceService {
|
||||
);
|
||||
}
|
||||
current = rerollExpiredTyrant(current, onlinePlayerIds, now);
|
||||
current = rerollExpiredVigilante(current, onlinePlayerIds, now);
|
||||
if (current.game().tyrantId().isEmpty() && current.game().pendingTyrant().isEmpty()) {
|
||||
current = selectTyrant(current, Optional.empty(), onlinePlayerIds, now);
|
||||
}
|
||||
if (current.game().tyrantId().isPresent()
|
||||
&& current.game().vigilanteId().isEmpty()
|
||||
&& current.game().pendingVigilante().isEmpty()) {
|
||||
current = rerollVigilante(
|
||||
current.asPersistentState(), Optional.empty(), onlinePlayerIds, now
|
||||
);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
@@ -74,17 +66,9 @@ public final class RoleMaintenanceService {
|
||||
Instant now
|
||||
) {
|
||||
Map<UUID, PlayerState> players = clearFollowers(state.players(), previousVigilante);
|
||||
Set<UUID> excluded = state.game().tyrantId().map(Set::of).orElseGet(Set::of);
|
||||
Optional<UUID> selected = candidates.select(
|
||||
players, excluded, previousVigilante, now, activityWindow, random
|
||||
);
|
||||
Optional<UUID> active = selected.filter(onlinePlayerIds::contains);
|
||||
Optional<PendingSelection> pending = selected
|
||||
.filter(playerId -> active.isEmpty())
|
||||
.map(playerId -> new PendingSelection(playerId, now.plus(pendingTimeout)));
|
||||
GameState game = copyRoles(
|
||||
state.game(), state.game().tyrantId(), active,
|
||||
state.game().pendingTyrant(), pending
|
||||
state.game(), state.game().tyrantId(), Optional.empty(),
|
||||
state.game().pendingTyrant(), Optional.empty()
|
||||
);
|
||||
return new LifecycleState(game, players);
|
||||
}
|
||||
@@ -109,16 +93,8 @@ public final class RoleMaintenanceService {
|
||||
pendingTyrant = Optional.empty();
|
||||
}
|
||||
}
|
||||
if (pendingVigilante.isPresent()) {
|
||||
PendingSelection pending = pendingVigilante.orElseThrow();
|
||||
if (!now.isAfter(pending.expiresAt())
|
||||
&& onlinePlayerIds.contains(pending.candidateId())
|
||||
&& eligibleAtConfirmation(state.players().get(pending.candidateId()), now)
|
||||
&& tyrant.filter(pending.candidateId()::equals).isEmpty()) {
|
||||
vigilante = Optional.of(pending.candidateId());
|
||||
pendingVigilante = Optional.empty();
|
||||
}
|
||||
}
|
||||
// Legacy pending Vigilante selections become arena vacancies.
|
||||
pendingVigilante = Optional.empty();
|
||||
return new LifecycleState(
|
||||
copyRoles(game, tyrant, vigilante, pendingTyrant, pendingVigilante),
|
||||
state.players()
|
||||
@@ -163,23 +139,6 @@ public final class RoleMaintenanceService {
|
||||
return new LifecycleState(game, state.players());
|
||||
}
|
||||
|
||||
private LifecycleState rerollExpiredVigilante(
|
||||
LifecycleState state,
|
||||
Set<UUID> onlinePlayerIds,
|
||||
Instant now
|
||||
) {
|
||||
Optional<PendingSelection> pending = state.game().pendingVigilante();
|
||||
if (pending.isEmpty() || !now.isAfter(pending.orElseThrow().expiresAt())) {
|
||||
return state;
|
||||
}
|
||||
return rerollVigilante(
|
||||
state.asPersistentState(),
|
||||
Optional.of(pending.orElseThrow().candidateId()),
|
||||
onlinePlayerIds,
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isInactive(
|
||||
Optional<UUID> roleHolder,
|
||||
Map<UUID, PlayerState> players,
|
||||
|
||||
@@ -16,6 +16,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
private FixerEffectController fixerEffects;
|
||||
private TyrantEffectController tyrantEffects;
|
||||
private OptOutGlowController optOutGlow;
|
||||
private VigilanteArenaController vigilanteArena;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -33,6 +34,14 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
ArenaLocationStore arenaLocations = new BukkitConfigArenaLocationStore(this);
|
||||
try {
|
||||
arenaLocations.location();
|
||||
} catch (IllegalArgumentException exception) {
|
||||
getLogger().log(Level.SEVERE, "Invalid Vigilante arena location", exception);
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
Clock clock = Clock.systemUTC();
|
||||
RoleCandidateSelector candidateSelector = new RoleCandidateSelector();
|
||||
java.util.random.RandomGenerator random = java.util.random.RandomGenerator.getDefault();
|
||||
@@ -131,10 +140,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
onlinePlayers,
|
||||
managedEffects,
|
||||
clock,
|
||||
candidateSelector,
|
||||
settings.candidateActivityWindow(),
|
||||
settings.pendingSelectionTimeout(),
|
||||
random
|
||||
arenaLocations
|
||||
));
|
||||
Objects.requireNonNull(getCommand("tyrant"), "Missing tyrant command metadata")
|
||||
.setTabCompleter(new TyrantTabCompleter(stateManager, onlinePlayers));
|
||||
@@ -148,6 +154,10 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
new PlayerJoinListener(stateManager, clock, tyrantPresentation),
|
||||
this
|
||||
);
|
||||
vigilanteArena = new VigilanteArenaController(
|
||||
stateManager, getServer(), arenaLocations, settings
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(vigilanteArena, this);
|
||||
getServer().getPluginManager().registerEvents(controlPanelRenderer, this);
|
||||
getServer().getPluginManager().registerEvents(vigilanteControlPanelRenderer, this);
|
||||
getServer().getPluginManager().registerEvents(
|
||||
@@ -243,6 +253,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
getServer().getScheduler().runTaskTimer(this, fixerEffects, 10L, 10L);
|
||||
getServer().getScheduler().runTaskTimer(this, tyrantEffects, 10L, 10L);
|
||||
getServer().getScheduler().runTaskTimer(this, optOutGlow, 10L, 10L);
|
||||
getServer().getScheduler().runTaskTimer(this, vigilanteArena, 1L, 10L);
|
||||
getServer().getScheduler().runTaskTimer(
|
||||
this,
|
||||
new AbilityItemRefreshTask(
|
||||
@@ -272,6 +283,9 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (vigilanteArena != null) {
|
||||
vigilanteArena.close();
|
||||
}
|
||||
if (optOutGlow != null) {
|
||||
optOutGlow.clearAll();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.random.RandomGenerator;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
@@ -20,10 +16,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
private final OnlinePlayerDirectory onlinePlayers;
|
||||
private final ManagedRoleEffects effects;
|
||||
private final Clock clock;
|
||||
private final RoleCandidateSelector candidateSelector;
|
||||
private final Duration activityWindow;
|
||||
private final Duration pendingTimeout;
|
||||
private final RandomGenerator random;
|
||||
private final ArenaLocationStore arenaLocations;
|
||||
|
||||
public TyrantAdminCommand(
|
||||
TyrantStateManager stateManager,
|
||||
@@ -34,8 +27,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
) {
|
||||
this(
|
||||
stateManager, lifecycle, onlinePlayers, effects, clock,
|
||||
new RoleCandidateSelector(), Duration.ofHours(24), Duration.ofHours(24),
|
||||
RandomGenerator.getDefault()
|
||||
ArenaLocationStore.unavailable()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,20 +37,14 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
OnlinePlayerDirectory onlinePlayers,
|
||||
ManagedRoleEffects effects,
|
||||
Clock clock,
|
||||
RoleCandidateSelector candidateSelector,
|
||||
Duration activityWindow,
|
||||
Duration pendingTimeout,
|
||||
RandomGenerator random
|
||||
ArenaLocationStore arenaLocations
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.lifecycle = lifecycle;
|
||||
this.onlinePlayers = onlinePlayers;
|
||||
this.effects = effects;
|
||||
this.clock = clock;
|
||||
this.candidateSelector = candidateSelector;
|
||||
this.activityWindow = activityWindow;
|
||||
this.pendingTimeout = pendingTimeout;
|
||||
this.random = random;
|
||||
this.arenaLocations = arenaLocations;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,6 +68,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
case "pause" -> pause(sender);
|
||||
case "resume" -> resume(sender);
|
||||
case "reset" -> reset(sender, arguments);
|
||||
case "arena" -> arena(sender, arguments);
|
||||
default -> usage(sender);
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
@@ -112,38 +99,17 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
stateManager.player(tyrant.getUniqueId(), tyrant.getName());
|
||||
vigilante.ifPresent(player -> stateManager.player(player.getUniqueId(), player.getName()));
|
||||
InstantHolder now = new InstantHolder(clock.instant());
|
||||
Optional<UUID> randomCandidate = vigilante.isEmpty()
|
||||
? candidateSelector.select(
|
||||
stateManager.players(), Set.of(tyrant.getUniqueId()), Optional.empty(),
|
||||
now.value(), activityWindow, random
|
||||
)
|
||||
: Optional.empty();
|
||||
Optional<UUID> activeVigilante = vigilante.map(Player::getUniqueId);
|
||||
if (activeVigilante.isEmpty() && randomCandidate.isPresent()
|
||||
&& onlinePlayers.findById(randomCandidate.orElseThrow()) != null) {
|
||||
activeVigilante = randomCandidate;
|
||||
}
|
||||
LifecycleState started = lifecycle.start(
|
||||
stateManager.snapshot(), tyrant.getUniqueId(), activeVigilante, now.value()
|
||||
stateManager.snapshot(), tyrant.getUniqueId(),
|
||||
vigilante.map(Player::getUniqueId), now.value()
|
||||
);
|
||||
if (activeVigilante.isEmpty() && randomCandidate.isPresent()) {
|
||||
GameState game = started.game();
|
||||
started = new LifecycleState(new GameState(
|
||||
game.lifecycle(), game.tyrantId(), game.vigilanteId(), game.pendingTyrant(),
|
||||
Optional.of(new PendingSelection(
|
||||
randomCandidate.orElseThrow(), now.value().plus(pendingTimeout)
|
||||
)),
|
||||
game.pausedAt(), game.accumulatedPausedTime(), game.tyrantLevel(),
|
||||
game.unspentChoices(), game.purchases()
|
||||
), started.players());
|
||||
}
|
||||
stateManager.replaceState(started);
|
||||
stateManager.saveIfDirty();
|
||||
effects.restoreAll();
|
||||
sender.sendMessage(ChatColor.GREEN + "Tyrant game started.");
|
||||
if (vigilante.isEmpty()) {
|
||||
sender.sendMessage(ChatColor.YELLOW
|
||||
+ "An eligible recently active Vigilante will be selected automatically.");
|
||||
+ "The Vigilante arena challenge is open.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +141,30 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
sender.sendMessage(ChatColor.GREEN + "Tyrant game reset.");
|
||||
}
|
||||
|
||||
private void arena(CommandSender sender, String[] arguments) {
|
||||
if (arguments.length == 1 || arguments[1].equalsIgnoreCase("status")) {
|
||||
sender.sendMessage("Vigilante arena: " + arenaLocations.location()
|
||||
.map(ArenaLocation::display).orElse("not configured"));
|
||||
return;
|
||||
}
|
||||
if (arguments.length != 2 || !arguments[1].equalsIgnoreCase("set")) {
|
||||
sender.sendMessage(ChatColor.YELLOW
|
||||
+ "Usage: /tyrantadmin arena <set|status>");
|
||||
return;
|
||||
}
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(ChatColor.RED + "Only a player can set the arena location.");
|
||||
return;
|
||||
}
|
||||
org.bukkit.Location location = player.getLocation();
|
||||
ArenaLocation arena = new ArenaLocation(
|
||||
location.getWorld().getName(), location.getX(), location.getY(), location.getZ(),
|
||||
location.getYaw(), location.getPitch()
|
||||
);
|
||||
arenaLocations.set(arena);
|
||||
sender.sendMessage(ChatColor.GREEN + "Vigilante arena set to " + arena.display() + ".");
|
||||
}
|
||||
|
||||
private void showStatus(CommandSender sender) {
|
||||
GameState game = stateManager.game();
|
||||
sender.sendMessage(ChatColor.GOLD + "Lifecycle: " + game.lifecycle());
|
||||
@@ -186,6 +176,8 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
+ ", purchases: " + game.purchases());
|
||||
sender.sendMessage("Pending Tyrant: " + game.pendingTyrant());
|
||||
sender.sendMessage("Pending Vigilante: " + game.pendingVigilante());
|
||||
sender.sendMessage("Vigilante arena: " + arenaLocations.location()
|
||||
.map(ArenaLocation::display).orElse("not configured"));
|
||||
for (PlayerState player : stateManager.players().values()) {
|
||||
boolean optedOut = player.optedOutUntil().isPresent();
|
||||
sender.sendMessage(
|
||||
@@ -202,6 +194,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>");
|
||||
+ "Usage: /tyrantadmin <status|start|pause|resume|reset confirm|arena set>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
"status", "start", "pause", "resume", "reset", "arena"
|
||||
);
|
||||
private final OnlinePlayerDirectory onlinePlayers;
|
||||
|
||||
@@ -37,6 +37,9 @@ public final class TyrantAdminTabCompleter implements TabCompleter {
|
||||
return switch (subcommand) {
|
||||
case "start" -> TabSuggestions.matching(arguments[1], onlineNames(null));
|
||||
case "reset" -> TabSuggestions.matching(arguments[1], List.of("confirm"));
|
||||
case "arena" -> TabSuggestions.matching(
|
||||
arguments[1], List.of("set", "status")
|
||||
);
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -365,7 +365,7 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
private void relinquish(Player player, String[] arguments) {
|
||||
if (arguments.length != 2 || !arguments[1].equalsIgnoreCase("confirm")) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "Relinquishing rerolls your role. Use /tyrant relinquish confirm.");
|
||||
+ "Relinquishing ends your role. Use /tyrant relinquish confirm.");
|
||||
return;
|
||||
}
|
||||
if (relinquishment == null) {
|
||||
|
||||
@@ -38,7 +38,6 @@ public final class TyrantSuccessionService {
|
||||
throw new IllegalStateException("a running Tyrant reign is required");
|
||||
}
|
||||
UUID formerTyrant = state.game().tyrantId().orElseThrow();
|
||||
Optional<UUID> formerVigilante = state.game().vigilanteId();
|
||||
Map<UUID, PlayerState> clearedPlayers = clearAssignments(state.players());
|
||||
Optional<UUID> selectedTyrant = killerId.isPresent()
|
||||
? killerId
|
||||
@@ -56,28 +55,12 @@ public final class TyrantSuccessionService {
|
||||
.filter(playerId -> activeTyrant.isEmpty())
|
||||
.map(playerId -> new PendingSelection(playerId, now.plus(pendingTimeout)));
|
||||
|
||||
Optional<UUID> selectedVigilante = activeTyrant.flatMap(tyrantId ->
|
||||
candidates.select(
|
||||
clearedPlayers,
|
||||
Set.of(tyrantId),
|
||||
formerVigilante,
|
||||
now,
|
||||
activityWindow,
|
||||
random
|
||||
)
|
||||
);
|
||||
Optional<UUID> activeVigilante = selectedVigilante
|
||||
.filter(onlinePlayerIds::contains);
|
||||
Optional<PendingSelection> pendingVigilante = selectedVigilante
|
||||
.filter(playerId -> activeVigilante.isEmpty())
|
||||
.map(playerId -> new PendingSelection(playerId, now.plus(pendingTimeout)));
|
||||
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING,
|
||||
activeTyrant,
|
||||
activeVigilante,
|
||||
Optional.empty(),
|
||||
pendingTyrant,
|
||||
pendingVigilante,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
state.game().accumulatedPausedTime(),
|
||||
0,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
final class VigilanteArenaBarrier {
|
||||
private final Map<Location, BlockData> replaced = new LinkedHashMap<>();
|
||||
private boolean built;
|
||||
|
||||
void build(World world, ArenaLocation center, double radius) {
|
||||
if (built) {
|
||||
return;
|
||||
}
|
||||
built = true;
|
||||
int samples = Math.max(64, (int) Math.ceil(2.0 * Math.PI * radius * 2.0));
|
||||
int baseY = (int) Math.floor(center.y());
|
||||
for (int index = 0; index < samples; index++) {
|
||||
double angle = 2.0 * Math.PI * index / samples;
|
||||
int x = (int) Math.floor(center.x() + Math.cos(angle) * radius);
|
||||
int z = (int) Math.floor(center.z() + Math.sin(angle) * radius);
|
||||
for (int y = baseY; y < baseY + 3; y++) {
|
||||
Block block = world.getBlockAt(x, y, z);
|
||||
Location location = block.getLocation();
|
||||
if (replaced.containsKey(location) || !isAir(block.getType())) {
|
||||
continue;
|
||||
}
|
||||
replaced.put(location, block.getBlockData().clone());
|
||||
block.setType(Material.BARRIER, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
for (Map.Entry<Location, BlockData> entry : replaced.entrySet()) {
|
||||
Location location = entry.getKey();
|
||||
World world = location.getWorld();
|
||||
if (world != null) {
|
||||
world.getBlockAt(location).setBlockData(entry.getValue(), false);
|
||||
}
|
||||
}
|
||||
replaced.clear();
|
||||
built = false;
|
||||
}
|
||||
|
||||
boolean active() {
|
||||
return built;
|
||||
}
|
||||
|
||||
private static boolean isAir(Material material) {
|
||||
return material == Material.AIR
|
||||
|| material == Material.CAVE_AIR
|
||||
|| material == Material.VOID_AIR;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Mob;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.entity.Zombie;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.event.entity.EntityCombustEvent;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDeathEvent;
|
||||
import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
public final class VigilanteArenaController implements Listener, Runnable {
|
||||
private static final String BOSS_TAG = "spigottyrant-vigilante-boss";
|
||||
private final TyrantStateManager stateManager;
|
||||
private final Server server;
|
||||
private final ArenaLocationStore locations;
|
||||
private final PluginSettings settings;
|
||||
private final VigilanteArenaSuccessionService succession =
|
||||
new VigilanteArenaSuccessionService();
|
||||
private final VigilanteArenaBarrier barrier = new VigilanteArenaBarrier();
|
||||
private final Set<UUID> relocating = new HashSet<>();
|
||||
private UUID bossId;
|
||||
private UUID challengerId;
|
||||
private boolean spawningBoss;
|
||||
private boolean wasOpen;
|
||||
private boolean warnedMissingLocation;
|
||||
private ArenaLocation observedLocation;
|
||||
|
||||
public VigilanteArenaController(
|
||||
TyrantStateManager stateManager,
|
||||
Server server,
|
||||
ArenaLocationStore locations,
|
||||
PluginSettings settings
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.server = server;
|
||||
this.locations = locations;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Optional<ArenaLocation> configured = locations.location();
|
||||
if (configured.isEmpty()) {
|
||||
closeFight();
|
||||
observedLocation = null;
|
||||
warnIfOpenWithoutLocation();
|
||||
wasOpen = false;
|
||||
return;
|
||||
}
|
||||
ArenaLocation arena = configured.orElseThrow();
|
||||
if (observedLocation != null && !observedLocation.equals(arena)) {
|
||||
closeFight();
|
||||
}
|
||||
observedLocation = arena;
|
||||
World world = server.getWorld(arena.worldName());
|
||||
if (world == null) {
|
||||
closeFight();
|
||||
warnIfOpenWithoutLocation();
|
||||
wasOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
warnedMissingLocation = false;
|
||||
drawBoundary(world, arena);
|
||||
removeOtherMobs(world, arena);
|
||||
boolean open = VigilanteArenaPolicy.isOpen(stateManager.game());
|
||||
if (open && !wasOpen) {
|
||||
announceVacancy(arena);
|
||||
}
|
||||
wasOpen = open;
|
||||
if (!open) {
|
||||
closeFight();
|
||||
return;
|
||||
}
|
||||
|
||||
Zombie boss = boss(world);
|
||||
if (boss == null) {
|
||||
boss = spawnBoss(world, arena);
|
||||
}
|
||||
boss.setFireTicks(0);
|
||||
if (!contains(arena, boss.getLocation())) {
|
||||
boss.teleport(toBukkit(world, arena));
|
||||
}
|
||||
Player challenger = challengerId == null ? null : server.getPlayer(challengerId);
|
||||
if (challenger == null || challenger.isDead()
|
||||
|| !contains(arena, challenger.getLocation())
|
||||
|| !VigilanteArenaPolicy.isEligible(
|
||||
stateManager.game(), playerState(challenger)
|
||||
)) {
|
||||
if (challengerId != null) {
|
||||
resetFight();
|
||||
}
|
||||
boss.setTarget(null);
|
||||
} else {
|
||||
boss.setTarget(challenger);
|
||||
}
|
||||
for (Player player : world.getPlayers()) {
|
||||
enforcePosition(player, null, player.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onMove(PlayerMoveEvent event) {
|
||||
if (event.getTo() != null && movedHorizontally(event.getFrom(), event.getTo())
|
||||
&& enforcePosition(event.getPlayer(), event.getFrom(), event.getTo())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onTeleport(PlayerTeleportEvent event) {
|
||||
if (event.getTo() != null
|
||||
&& enforcePosition(event.getPlayer(), event.getFrom(), event.getTo())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onRespawn(PlayerRespawnEvent event) {
|
||||
Optional<ArenaLocation> configured = locations.location();
|
||||
if (configured.isPresent() && contains(
|
||||
configured.orElseThrow(), event.getRespawnLocation())) {
|
||||
event.setRespawnLocation(outside(configured.orElseThrow(), event.getRespawnLocation()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||
if (event.getEntity().getUniqueId().equals(challengerId)) {
|
||||
resetFight();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
if (event.getPlayer().getUniqueId().equals(challengerId)) {
|
||||
resetFight();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
if (!VigilanteArenaPolicy.isOpen(stateManager.game())) {
|
||||
return;
|
||||
}
|
||||
PlayerState state = playerState(event.getPlayer());
|
||||
locations.location().filter(location -> VigilanteArenaPolicy.isEligible(
|
||||
stateManager.game(), state
|
||||
)).ifPresent(location -> event.getPlayer().sendMessage(vacancyMessage(location)));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onCreatureSpawn(CreatureSpawnEvent event) {
|
||||
Optional<ArenaLocation> configured = locations.location();
|
||||
if (!spawningBoss && configured.isPresent()
|
||||
&& contains(configured.orElseThrow(), event.getLocation())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onCombust(EntityCombustEvent event) {
|
||||
if (event.getEntity().getUniqueId().equals(bossId)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onTarget(EntityTargetLivingEntityEvent event) {
|
||||
if (!event.getEntity().getUniqueId().equals(bossId)) {
|
||||
return;
|
||||
}
|
||||
Entity target = event.getTarget();
|
||||
if (target != null && (challengerId == null
|
||||
|| !target.getUniqueId().equals(challengerId))) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDamage(EntityDamageByEntityEvent event) {
|
||||
if (!event.getEntity().getUniqueId().equals(bossId)) {
|
||||
return;
|
||||
}
|
||||
Player attacker = attackingPlayer(event.getDamager());
|
||||
if (attacker == null || challengerId == null
|
||||
|| !attacker.getUniqueId().equals(challengerId)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onBossDeath(EntityDeathEvent event) {
|
||||
if (!event.getEntity().getUniqueId().equals(bossId)) {
|
||||
return;
|
||||
}
|
||||
UUID defeatedBoss = bossId;
|
||||
bossId = null;
|
||||
Player killer = event.getEntity().getKiller();
|
||||
if (killer != null && challengerId != null
|
||||
&& killer.getUniqueId().equals(challengerId)
|
||||
&& VigilanteArenaPolicy.isEligible(
|
||||
stateManager.game(), playerState(killer)
|
||||
)) {
|
||||
assignVigilante(challengerId);
|
||||
return;
|
||||
}
|
||||
bossId = defeatedBoss;
|
||||
resetFight();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
closeFight();
|
||||
}
|
||||
|
||||
private boolean enforcePosition(Player player, Location from, Location to) {
|
||||
Optional<ArenaLocation> configured = locations.location();
|
||||
if (configured.isEmpty() || relocating.contains(player.getUniqueId())) {
|
||||
return false;
|
||||
}
|
||||
ArenaLocation arena = configured.orElseThrow();
|
||||
boolean inside = contains(arena, to);
|
||||
if (player.getUniqueId().equals(challengerId)) {
|
||||
if (!inside) {
|
||||
resetFight();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!inside) {
|
||||
return false;
|
||||
}
|
||||
PlayerState state = playerState(player);
|
||||
if (challengerId == null && bossId != null
|
||||
&& VigilanteArenaPolicy.isEligible(stateManager.game(), state)) {
|
||||
challengerId = player.getUniqueId();
|
||||
World world = server.getWorld(arena.worldName());
|
||||
if (world != null) {
|
||||
barrier.build(world, arena, settings.vigilanteArenaRadiusBlocks());
|
||||
Zombie boss = boss(world);
|
||||
if (boss != null) {
|
||||
boss.setHealth(settings.vigilanteBossHealth());
|
||||
boss.teleport(toBukkit(world, arena));
|
||||
boss.setTarget(player);
|
||||
}
|
||||
}
|
||||
player.sendMessage(ChatColor.GOLD + "Defeat the arena boss to become Vigilante!");
|
||||
return false;
|
||||
}
|
||||
bounce(player, from, to, arena);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void bounce(Player player, Location from, Location attempted, ArenaLocation arena) {
|
||||
Location destination = from != null && !contains(arena, from)
|
||||
? from.clone() : outside(arena, attempted);
|
||||
relocating.add(player.getUniqueId());
|
||||
try {
|
||||
player.teleport(destination, PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
} finally {
|
||||
relocating.remove(player.getUniqueId());
|
||||
}
|
||||
player.sendMessage(ChatColor.RED + "Only the active challenger may enter the arena.");
|
||||
}
|
||||
|
||||
private void resetFight() {
|
||||
challengerId = null;
|
||||
barrier.clear();
|
||||
Optional<ArenaLocation> configured = locations.location();
|
||||
if (configured.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
World world = server.getWorld(configured.orElseThrow().worldName());
|
||||
Zombie boss = world == null ? null : boss(world);
|
||||
if (boss != null && !boss.isDead()) {
|
||||
boss.setHealth(settings.vigilanteBossHealth());
|
||||
boss.teleport(toBukkit(world, configured.orElseThrow()));
|
||||
boss.setTarget(null);
|
||||
} else {
|
||||
bossId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void closeFight() {
|
||||
challengerId = null;
|
||||
barrier.clear();
|
||||
if (bossId != null) {
|
||||
Entity entity = server.getEntity(bossId);
|
||||
if (entity != null) {
|
||||
entity.remove();
|
||||
}
|
||||
bossId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private Zombie spawnBoss(World world, ArenaLocation arena) {
|
||||
for (Entity entity : world.getEntities()) {
|
||||
if (entity.getScoreboardTags().contains(BOSS_TAG)) {
|
||||
entity.remove();
|
||||
}
|
||||
}
|
||||
spawningBoss = true;
|
||||
try {
|
||||
Zombie boss = world.spawn(toBukkit(world, arena), Zombie.class, zombie -> {
|
||||
zombie.setAdult();
|
||||
zombie.setCustomName(ChatColor.DARK_RED + "Vigilante's Trial");
|
||||
zombie.setCustomNameVisible(true);
|
||||
zombie.setRemoveWhenFarAway(false);
|
||||
zombie.setCanPickupItems(false);
|
||||
zombie.addScoreboardTag(BOSS_TAG);
|
||||
setAttribute(zombie, Attribute.MAX_HEALTH, settings.vigilanteBossHealth());
|
||||
setAttribute(zombie, Attribute.ATTACK_DAMAGE, settings.vigilanteBossDamage());
|
||||
setAttribute(zombie, Attribute.ARMOR, settings.vigilanteBossArmor());
|
||||
zombie.setHealth(settings.vigilanteBossHealth());
|
||||
});
|
||||
bossId = boss.getUniqueId();
|
||||
return boss;
|
||||
} finally {
|
||||
spawningBoss = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Zombie boss(World world) {
|
||||
if (bossId == null) {
|
||||
return null;
|
||||
}
|
||||
Entity entity = server.getEntity(bossId);
|
||||
return entity instanceof Zombie zombie && entity.getWorld().equals(world)
|
||||
&& !zombie.isDead() ? zombie : null;
|
||||
}
|
||||
|
||||
private void assignVigilante(UUID playerId) {
|
||||
LifecycleState assigned;
|
||||
try {
|
||||
assigned = succession.claim(stateManager.snapshot(), playerId);
|
||||
} catch (IllegalStateException exception) {
|
||||
resetFight();
|
||||
return;
|
||||
}
|
||||
stateManager.replaceState(assigned);
|
||||
stateManager.saveIfDirty();
|
||||
challengerId = null;
|
||||
barrier.clear();
|
||||
String name = Optional.ofNullable(server.getPlayer(playerId))
|
||||
.map(Player::getName).orElse(playerId.toString());
|
||||
String message = ChatColor.YELLOW + name + " defeated the arena boss and is the Vigilante!";
|
||||
server.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
||||
}
|
||||
|
||||
private void announceVacancy(ArenaLocation arena) {
|
||||
String message = vacancyMessage(arena);
|
||||
for (Player player : server.getOnlinePlayers()) {
|
||||
PlayerState state = playerState(player);
|
||||
if (VigilanteArenaPolicy.isEligible(stateManager.game(), state)) {
|
||||
player.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void warnIfOpenWithoutLocation() {
|
||||
if (!VigilanteArenaPolicy.isOpen(stateManager.game()) || warnedMissingLocation) {
|
||||
return;
|
||||
}
|
||||
warnedMissingLocation = true;
|
||||
server.getConsoleSender().sendMessage(ChatColor.RED
|
||||
+ "The Vigilante role is vacant, but no valid arena location is configured. "
|
||||
+ "Use /tyrantadmin arena set.");
|
||||
for (Player player : server.getOnlinePlayers()) {
|
||||
if (player.hasPermission("spigottyrant.admin")) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "The Vigilante arena location is not configured.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String vacancyMessage(ArenaLocation arena) {
|
||||
return ChatColor.YELLOW + "The Vigilante position is open! Challenge the boss in "
|
||||
+ arena.display() + ".";
|
||||
}
|
||||
|
||||
private void removeOtherMobs(World world, ArenaLocation arena) {
|
||||
Location center = toBukkit(world, arena);
|
||||
double radius = settings.vigilanteArenaRadiusBlocks();
|
||||
for (Entity entity : world.getNearbyEntities(center, radius, world.getMaxHeight(), radius)) {
|
||||
if (entity instanceof Mob && !entity.getUniqueId().equals(bossId)
|
||||
&& contains(arena, entity.getLocation())) {
|
||||
entity.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBoundary(World world, ArenaLocation arena) {
|
||||
if (barrier.active()) {
|
||||
return;
|
||||
}
|
||||
double radius = settings.vigilanteArenaRadiusBlocks();
|
||||
for (int index = 0; index < 48; index++) {
|
||||
double angle = 2.0 * Math.PI * index / 48.0;
|
||||
Location point = new Location(
|
||||
world,
|
||||
arena.x() + Math.cos(angle) * radius,
|
||||
arena.y() + 1.0,
|
||||
arena.z() + Math.sin(angle) * radius
|
||||
);
|
||||
world.spawnParticle(Particle.END_ROD, point, 1, 0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean contains(ArenaLocation arena, Location location) {
|
||||
World world = location.getWorld();
|
||||
return world != null && ArenaGeometry.contains(
|
||||
arena, settings.vigilanteArenaRadiusBlocks(), world.getName(),
|
||||
location.getX(), location.getY(), location.getZ()
|
||||
);
|
||||
}
|
||||
|
||||
private Location outside(ArenaLocation arena, Location attempted) {
|
||||
ArenaPoint point = ArenaGeometry.outside(
|
||||
arena, settings.vigilanteArenaRadiusBlocks(), attempted.getX(),
|
||||
attempted.getY(), attempted.getZ()
|
||||
);
|
||||
World world = server.getWorld(arena.worldName());
|
||||
if (world == null) {
|
||||
return attempted;
|
||||
}
|
||||
int x = (int) Math.floor(point.x());
|
||||
int z = (int) Math.floor(point.z());
|
||||
int preferredY = (int) Math.floor(point.y());
|
||||
for (int offset = 0; offset <= 8; offset++) {
|
||||
int[] candidates = offset == 0
|
||||
? new int[] {preferredY} : new int[] {preferredY + offset, preferredY - offset};
|
||||
for (int y : candidates) {
|
||||
if (y > world.getMinHeight() && y + 1 < world.getMaxHeight()
|
||||
&& world.getBlockAt(x, y, z).isPassable()
|
||||
&& world.getBlockAt(x, y + 1, z).isPassable()
|
||||
&& !world.getBlockAt(x, y - 1, z).isPassable()) {
|
||||
return new Location(world, x + 0.5, y, z + 0.5,
|
||||
attempted.getYaw(), attempted.getPitch());
|
||||
}
|
||||
}
|
||||
}
|
||||
int surfaceY = Math.min(
|
||||
world.getMaxHeight() - 2,
|
||||
Math.max(world.getMinHeight() + 1, world.getHighestBlockYAt(x, z) + 1)
|
||||
);
|
||||
return new Location(world, x + 0.5, surfaceY, z + 0.5,
|
||||
attempted.getYaw(), attempted.getPitch());
|
||||
}
|
||||
|
||||
private PlayerState playerState(Player player) {
|
||||
PlayerState existing = stateManager.players().get(player.getUniqueId());
|
||||
return existing == null
|
||||
? stateManager.player(player.getUniqueId(), player.getName()) : existing;
|
||||
}
|
||||
|
||||
private static Location toBukkit(World world, ArenaLocation arena) {
|
||||
return new Location(
|
||||
world, arena.x(), arena.y(), arena.z(), arena.yaw(), arena.pitch()
|
||||
);
|
||||
}
|
||||
|
||||
private static Player attackingPlayer(Entity damager) {
|
||||
if (damager instanceof Player player) {
|
||||
return player;
|
||||
}
|
||||
if (damager instanceof Projectile projectile
|
||||
&& projectile.getShooter() instanceof Player player) {
|
||||
return player;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void setAttribute(Zombie zombie, Attribute attribute, double value) {
|
||||
if (zombie.getAttribute(attribute) != null) {
|
||||
zombie.getAttribute(attribute).setBaseValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean movedHorizontally(Location from, Location to) {
|
||||
return from.getWorld() != to.getWorld()
|
||||
|| from.getX() != to.getX() || from.getZ() != to.getZ();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
public final class VigilanteArenaPolicy {
|
||||
private VigilanteArenaPolicy() {
|
||||
}
|
||||
|
||||
public static boolean isOpen(GameState game) {
|
||||
return game.lifecycle() == GameLifecycle.RUNNING
|
||||
&& game.tyrantId().isPresent()
|
||||
&& game.vigilanteId().isEmpty();
|
||||
}
|
||||
|
||||
public static boolean isEligible(GameState game, PlayerState player) {
|
||||
return player != null
|
||||
&& isOpen(game)
|
||||
&& player.optedOutUntil().isEmpty()
|
||||
&& game.tyrantId().filter(player.playerId()::equals).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class VigilanteArenaSuccessionService {
|
||||
public LifecycleState claim(PersistentState state, UUID winnerId) {
|
||||
PlayerState winner = state.players().get(winnerId);
|
||||
if (!VigilanteArenaPolicy.isEligible(state.game(), winner)) {
|
||||
throw new IllegalStateException("player is not eligible to claim the Vigilante role");
|
||||
}
|
||||
GameState game = state.game();
|
||||
GameState assigned = new GameState(
|
||||
game.lifecycle(), game.tyrantId(), Optional.of(winnerId), game.pendingTyrant(),
|
||||
Optional.empty(), game.pausedAt(), game.accumulatedPausedTime(),
|
||||
game.tyrantLevel(), game.unspentChoices(), game.purchases()
|
||||
);
|
||||
return new LifecycleState(assigned, state.players());
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,8 @@ public final class VigilanteDeathListener implements Listener {
|
||||
);
|
||||
stateManager.replaceState(rerolled);
|
||||
stateManager.saveIfDirty();
|
||||
String message = ChatColor.YELLOW + "The Vigilante died; a replacement "
|
||||
+ (rerolled.game().vigilanteId().isPresent() ? "has been selected." : "is pending.");
|
||||
String message = ChatColor.YELLOW
|
||||
+ "The Vigilante died; the arena challenge is now open.";
|
||||
server.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user