feat(tyrant): add arena boss succession
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
public enum ArenaRole {
|
||||
TYRANT,
|
||||
VIGILANTE
|
||||
}
|
||||
@@ -18,9 +18,7 @@ public record PluginSettings(
|
||||
double vigilanteBossArmor,
|
||||
Duration optOutDuration,
|
||||
Duration roleInactivity,
|
||||
Duration candidateActivityWindow,
|
||||
Duration pendingSelectionTimeout,
|
||||
Duration selectionRetryInterval,
|
||||
Duration roleMaintenanceInterval,
|
||||
Duration assassinCooldown,
|
||||
Duration assassinInvisibilityDuration,
|
||||
Duration assassinDoubleJumpCooldown,
|
||||
@@ -60,9 +58,7 @@ public record PluginSettings(
|
||||
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");
|
||||
requirePositive(pendingSelectionTimeout, "pending-selection-timeout-seconds");
|
||||
requirePositive(selectionRetryInterval, "selection-retry-seconds");
|
||||
requirePositive(roleMaintenanceInterval, "role-maintenance-seconds");
|
||||
requirePositive(assassinCooldown, "assassin-cooldown-seconds");
|
||||
requirePositive(assassinInvisibilityDuration, "assassin-invisibility-seconds");
|
||||
requirePositive(assassinDoubleJumpCooldown, "assassin-double-jump-cooldown-seconds");
|
||||
@@ -109,9 +105,10 @@ public record PluginSettings(
|
||||
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)),
|
||||
duration(values, "pending-selection-timeout-seconds", Duration.ofHours(24)),
|
||||
duration(values, "selection-retry-seconds", Duration.ofMinutes(1)),
|
||||
durationAlias(
|
||||
values, "role-maintenance-seconds", "selection-retry-seconds",
|
||||
Duration.ofMinutes(1)
|
||||
),
|
||||
duration(values, "assassin-cooldown-seconds", Duration.ofHours(1)),
|
||||
duration(values, "assassin-invisibility-seconds", Duration.ofMinutes(10)),
|
||||
duration(values, "assassin-double-jump-cooldown-seconds", Duration.ofSeconds(60)),
|
||||
@@ -289,6 +286,18 @@ public record PluginSettings(
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static Duration durationAlias(
|
||||
Map<String, ?> values,
|
||||
String key,
|
||||
String legacyKey,
|
||||
Duration defaultValue
|
||||
) {
|
||||
if (values.containsKey(key)) {
|
||||
return duration(values, key, defaultValue);
|
||||
}
|
||||
return duration(values, legacyKey, defaultValue);
|
||||
}
|
||||
|
||||
private static Duration duration(Map<String, ?> values, String key, Duration defaultValue) {
|
||||
Object raw = values.get(key);
|
||||
if (raw == null) {
|
||||
|
||||
+99
-51
@@ -5,6 +5,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Server;
|
||||
@@ -31,24 +32,27 @@ 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";
|
||||
public final class RoleArenaController implements Listener, Runnable {
|
||||
private static final String BOSS_TAG = "spigottyrant-role-arena-boss";
|
||||
private final TyrantStateManager stateManager;
|
||||
private final Server server;
|
||||
private final ArenaLocationStore locations;
|
||||
private final PluginSettings settings;
|
||||
private final VigilanteArenaSuccessionService succession =
|
||||
private final VigilanteArenaSuccessionService vigilanteSuccession =
|
||||
new VigilanteArenaSuccessionService();
|
||||
private final TyrantArenaSuccessionService tyrantSuccession =
|
||||
new TyrantArenaSuccessionService();
|
||||
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 ArenaRole previousOpenRole;
|
||||
private ArenaRole bossRole;
|
||||
private ArenaRole warnedMissingRole;
|
||||
private ArenaLocation observedLocation;
|
||||
|
||||
public VigilanteArenaController(
|
||||
public RoleArenaController(
|
||||
TyrantStateManager stateManager,
|
||||
Server server,
|
||||
ArenaLocationStore locations,
|
||||
@@ -62,12 +66,13 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Optional<ArenaRole> openRole = RoleArenaPolicy.openRole(stateManager.game());
|
||||
Optional<ArenaLocation> configured = locations.location();
|
||||
if (configured.isEmpty()) {
|
||||
closeFight();
|
||||
observedLocation = null;
|
||||
warnIfOpenWithoutLocation();
|
||||
wasOpen = false;
|
||||
warnIfOpenWithoutLocation(openRole);
|
||||
previousOpenRole = null;
|
||||
return;
|
||||
}
|
||||
ArenaLocation arena = configured.orElseThrow();
|
||||
@@ -78,27 +83,31 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
World world = server.getWorld(arena.worldName());
|
||||
if (world == null) {
|
||||
closeFight();
|
||||
warnIfOpenWithoutLocation();
|
||||
wasOpen = false;
|
||||
warnIfOpenWithoutLocation(openRole);
|
||||
previousOpenRole = null;
|
||||
return;
|
||||
}
|
||||
|
||||
warnedMissingLocation = false;
|
||||
drawBoundary(world, arena);
|
||||
removeOtherMobs(world, arena);
|
||||
boolean open = VigilanteArenaPolicy.isOpen(stateManager.game());
|
||||
if (open && !wasOpen) {
|
||||
announceVacancy(arena);
|
||||
warnedMissingRole = null;
|
||||
ArenaRole role = openRole.orElse(null);
|
||||
if (role != previousOpenRole) {
|
||||
closeFight();
|
||||
if (role != null) {
|
||||
announceVacancy(arena, role);
|
||||
}
|
||||
}
|
||||
wasOpen = open;
|
||||
if (!open) {
|
||||
previousOpenRole = role;
|
||||
drawBoundary(world, arena, role);
|
||||
removeOtherMobs(world, arena);
|
||||
if (role == null) {
|
||||
closeFight();
|
||||
return;
|
||||
}
|
||||
|
||||
Zombie boss = boss(world);
|
||||
if (boss == null) {
|
||||
boss = spawnBoss(world, arena);
|
||||
if (boss == null || bossRole != role) {
|
||||
closeFight();
|
||||
boss = spawnBoss(world, arena, role);
|
||||
}
|
||||
boss.setFireTicks(0);
|
||||
if (!contains(arena, boss.getLocation())) {
|
||||
@@ -107,8 +116,8 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
Player challenger = challengerId == null ? null : server.getPlayer(challengerId);
|
||||
if (challenger == null || challenger.isDead()
|
||||
|| !contains(arena, challenger.getLocation())
|
||||
|| !VigilanteArenaPolicy.isEligible(
|
||||
stateManager.game(), playerState(challenger)
|
||||
|| !RoleArenaPolicy.isEligible(
|
||||
stateManager.game(), role, playerState(challenger)
|
||||
)) {
|
||||
if (challengerId != null) {
|
||||
resetFight();
|
||||
@@ -163,13 +172,16 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
if (!VigilanteArenaPolicy.isOpen(stateManager.game())) {
|
||||
Optional<ArenaRole> role = RoleArenaPolicy.openRole(stateManager.game());
|
||||
if (role.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
PlayerState state = playerState(event.getPlayer());
|
||||
locations.location().filter(location -> VigilanteArenaPolicy.isEligible(
|
||||
stateManager.game(), state
|
||||
)).ifPresent(location -> event.getPlayer().sendMessage(vacancyMessage(location)));
|
||||
locations.location().filter(location -> RoleArenaPolicy.isEligible(
|
||||
stateManager.game(), role.orElseThrow(), state
|
||||
)).ifPresent(location -> event.getPlayer().sendMessage(
|
||||
vacancyMessage(location, role.orElseThrow())
|
||||
));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
@@ -218,17 +230,20 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
return;
|
||||
}
|
||||
UUID defeatedBoss = bossId;
|
||||
ArenaRole defeatedRole = bossRole;
|
||||
bossId = null;
|
||||
bossRole = null;
|
||||
Player killer = event.getEntity().getKiller();
|
||||
if (killer != null && challengerId != null
|
||||
if (killer != null && challengerId != null && defeatedRole != null
|
||||
&& killer.getUniqueId().equals(challengerId)
|
||||
&& VigilanteArenaPolicy.isEligible(
|
||||
stateManager.game(), playerState(killer)
|
||||
&& RoleArenaPolicy.isEligible(
|
||||
stateManager.game(), defeatedRole, playerState(killer)
|
||||
)) {
|
||||
assignVigilante(challengerId);
|
||||
assignRole(challengerId, defeatedRole);
|
||||
return;
|
||||
}
|
||||
bossId = defeatedBoss;
|
||||
bossRole = defeatedRole;
|
||||
resetFight();
|
||||
}
|
||||
|
||||
@@ -253,8 +268,9 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
return false;
|
||||
}
|
||||
PlayerState state = playerState(player);
|
||||
if (challengerId == null && bossId != null
|
||||
&& VigilanteArenaPolicy.isEligible(stateManager.game(), state)) {
|
||||
ArenaRole role = bossRole;
|
||||
if (challengerId == null && bossId != null && role != null
|
||||
&& RoleArenaPolicy.isEligible(stateManager.game(), role, state)) {
|
||||
challengerId = player.getUniqueId();
|
||||
World world = server.getWorld(arena.worldName());
|
||||
if (world != null) {
|
||||
@@ -266,7 +282,8 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
boss.setTarget(player);
|
||||
}
|
||||
}
|
||||
player.sendMessage(ChatColor.GOLD + "Defeat the arena boss to become Vigilante!");
|
||||
player.sendMessage(ChatColor.GOLD + "Defeat the arena boss to become "
|
||||
+ readable(role) + "!");
|
||||
return false;
|
||||
}
|
||||
bounce(player, from, to, arena);
|
||||
@@ -313,9 +330,10 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
}
|
||||
bossId = null;
|
||||
}
|
||||
bossRole = null;
|
||||
}
|
||||
|
||||
private Zombie spawnBoss(World world, ArenaLocation arena) {
|
||||
private Zombie spawnBoss(World world, ArenaLocation arena, ArenaRole role) {
|
||||
for (Entity entity : world.getEntities()) {
|
||||
if (entity.getScoreboardTags().contains(BOSS_TAG)) {
|
||||
entity.remove();
|
||||
@@ -325,7 +343,9 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
try {
|
||||
Zombie boss = world.spawn(toBukkit(world, arena), Zombie.class, zombie -> {
|
||||
zombie.setAdult();
|
||||
zombie.setCustomName(ChatColor.DARK_RED + "Vigilante's Trial");
|
||||
zombie.setCustomName(role == ArenaRole.TYRANT
|
||||
? ChatColor.RED + "Tyrant's Trial"
|
||||
: ChatColor.BLUE + "Vigilante's Trial");
|
||||
zombie.setCustomNameVisible(true);
|
||||
zombie.setRemoveWhenFarAway(false);
|
||||
zombie.setCanPickupItems(false);
|
||||
@@ -336,6 +356,7 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
zombie.setHealth(settings.vigilanteBossHealth());
|
||||
});
|
||||
bossId = boss.getUniqueId();
|
||||
bossRole = role;
|
||||
return boss;
|
||||
} finally {
|
||||
spawningBoss = false;
|
||||
@@ -351,10 +372,12 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
&& !zombie.isDead() ? zombie : null;
|
||||
}
|
||||
|
||||
private void assignVigilante(UUID playerId) {
|
||||
private void assignRole(UUID playerId, ArenaRole role) {
|
||||
LifecycleState assigned;
|
||||
try {
|
||||
assigned = succession.claim(stateManager.snapshot(), playerId);
|
||||
assigned = role == ArenaRole.TYRANT
|
||||
? tyrantSuccession.claim(stateManager.snapshot(), playerId)
|
||||
: vigilanteSuccession.claim(stateManager.snapshot(), playerId);
|
||||
} catch (IllegalStateException exception) {
|
||||
resetFight();
|
||||
return;
|
||||
@@ -365,39 +388,46 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
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!";
|
||||
String message = ChatColor.YELLOW + name + " defeated the arena boss and is the "
|
||||
+ readable(role) + "!";
|
||||
server.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
||||
}
|
||||
|
||||
private void announceVacancy(ArenaLocation arena) {
|
||||
String message = vacancyMessage(arena);
|
||||
private void announceVacancy(ArenaLocation arena, ArenaRole role) {
|
||||
String message = vacancyMessage(arena, role);
|
||||
for (Player player : server.getOnlinePlayers()) {
|
||||
PlayerState state = playerState(player);
|
||||
if (VigilanteArenaPolicy.isEligible(stateManager.game(), state)) {
|
||||
if (RoleArenaPolicy.isEligible(stateManager.game(), role, state)) {
|
||||
player.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void warnIfOpenWithoutLocation() {
|
||||
if (!VigilanteArenaPolicy.isOpen(stateManager.game()) || warnedMissingLocation) {
|
||||
private void warnIfOpenWithoutLocation(Optional<ArenaRole> openRole) {
|
||||
if (openRole.isEmpty()) {
|
||||
warnedMissingRole = null;
|
||||
return;
|
||||
}
|
||||
warnedMissingLocation = true;
|
||||
ArenaRole role = openRole.orElseThrow();
|
||||
if (role == warnedMissingRole) {
|
||||
return;
|
||||
}
|
||||
warnedMissingRole = role;
|
||||
server.getConsoleSender().sendMessage(ChatColor.RED
|
||||
+ "The Vigilante role is vacant, but no valid arena location is configured. "
|
||||
+ "The " + readable(role)
|
||||
+ " 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.");
|
||||
+ "The shared role 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 String vacancyMessage(ArenaLocation arena, ArenaRole role) {
|
||||
return ChatColor.YELLOW + "The " + readable(role)
|
||||
+ " position is open! Challenge the boss in " + arena.display() + ".";
|
||||
}
|
||||
|
||||
private void removeOtherMobs(World world, ArenaLocation arena) {
|
||||
@@ -411,11 +441,14 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBoundary(World world, ArenaLocation arena) {
|
||||
private void drawBoundary(World world, ArenaLocation arena, ArenaRole role) {
|
||||
if (barrier.active()) {
|
||||
return;
|
||||
}
|
||||
double radius = settings.vigilanteArenaRadiusBlocks();
|
||||
Color color = boundaryColor(role);
|
||||
Particle.DustOptions dust = color == null
|
||||
? null : new Particle.DustOptions(color, 1.25F);
|
||||
for (int index = 0; index < 48; index++) {
|
||||
double angle = 2.0 * Math.PI * index / 48.0;
|
||||
Location point = new Location(
|
||||
@@ -424,7 +457,11 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
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);
|
||||
if (dust == null) {
|
||||
world.spawnParticle(Particle.END_ROD, point, 1, 0.0, 0.0, 0.0, 0.0);
|
||||
} else {
|
||||
world.spawnParticle(Particle.DUST, point, 1, 0.0, 0.0, 0.0, 0.0, dust);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +535,17 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
static Color boundaryColor(ArenaRole role) {
|
||||
if (role == null) {
|
||||
return null;
|
||||
}
|
||||
return role == ArenaRole.TYRANT ? Color.RED : Color.BLUE;
|
||||
}
|
||||
|
||||
private static String readable(ArenaRole role) {
|
||||
return role == ArenaRole.TYRANT ? "Tyrant" : "Vigilante";
|
||||
}
|
||||
|
||||
private static boolean movedHorizontally(Location from, Location to) {
|
||||
return from.getWorld() != to.getWorld()
|
||||
|| from.getX() != to.getX() || from.getZ() != to.getZ();
|
||||
@@ -0,0 +1,31 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public final class RoleArenaPolicy {
|
||||
private RoleArenaPolicy() {
|
||||
}
|
||||
|
||||
public static Optional<ArenaRole> openRole(GameState game) {
|
||||
if (game.lifecycle() != GameLifecycle.RUNNING) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (game.tyrantId().isEmpty()) {
|
||||
return game.pendingTyrant().isEmpty()
|
||||
? Optional.of(ArenaRole.TYRANT) : Optional.empty();
|
||||
}
|
||||
return game.vigilanteId().isEmpty()
|
||||
? Optional.of(ArenaRole.VIGILANTE) : Optional.empty();
|
||||
}
|
||||
|
||||
public static boolean isEligible(
|
||||
GameState game,
|
||||
ArenaRole role,
|
||||
PlayerState player
|
||||
) {
|
||||
return player != null
|
||||
&& openRole(game).filter(role::equals).isPresent()
|
||||
&& player.optedOutUntil().isEmpty()
|
||||
&& game.tyrantId().filter(player.playerId()::equals).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
public final class RoleCandidateSelector {
|
||||
public Optional<UUID> select(
|
||||
Map<UUID, PlayerState> players,
|
||||
Set<UUID> excluded,
|
||||
Optional<UUID> previousHolder,
|
||||
Instant now,
|
||||
Duration activityWindow,
|
||||
RandomGenerator random
|
||||
) {
|
||||
Instant cutoff = now.minus(activityWindow);
|
||||
List<UUID> eligible = new ArrayList<>();
|
||||
for (PlayerState player : players.values()) {
|
||||
if (excluded.contains(player.playerId())) {
|
||||
continue;
|
||||
}
|
||||
if (player.lastLogin().isEmpty()
|
||||
|| player.lastLogin().orElseThrow().isBefore(cutoff)) {
|
||||
continue;
|
||||
}
|
||||
if (player.optedOutUntil().isPresent()) {
|
||||
continue;
|
||||
}
|
||||
eligible.add(player.playerId());
|
||||
}
|
||||
if (eligible.size() > 1) {
|
||||
previousHolder.ifPresent(eligible::remove);
|
||||
}
|
||||
if (eligible.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
eligible.sort(Comparator.comparing(UUID::toString));
|
||||
return Optional.of(eligible.get(random.nextInt(eligible.size())));
|
||||
}
|
||||
}
|
||||
@@ -5,65 +5,39 @@ import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
public final class RoleMaintenanceService {
|
||||
private final RoleCandidateSelector candidates;
|
||||
private final TyrantSuccessionService succession;
|
||||
private final Duration inactivity;
|
||||
private final Duration activityWindow;
|
||||
private final Duration pendingTimeout;
|
||||
private final RandomGenerator random;
|
||||
|
||||
public RoleMaintenanceService(
|
||||
RoleCandidateSelector candidates,
|
||||
TyrantSuccessionService succession,
|
||||
Duration inactivity,
|
||||
Duration activityWindow,
|
||||
Duration pendingTimeout,
|
||||
RandomGenerator random
|
||||
Duration inactivity
|
||||
) {
|
||||
this.candidates = candidates;
|
||||
this.succession = succession;
|
||||
this.inactivity = inactivity;
|
||||
this.activityWindow = activityWindow;
|
||||
this.pendingTimeout = pendingTimeout;
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
public LifecycleState maintain(
|
||||
PersistentState state,
|
||||
Set<UUID> onlinePlayerIds,
|
||||
Instant now
|
||||
) {
|
||||
public LifecycleState maintain(PersistentState state, Instant now) {
|
||||
if (state.game().lifecycle() != GameLifecycle.RUNNING) {
|
||||
return new LifecycleState(state.game(), state.players());
|
||||
}
|
||||
LifecycleState current = confirmPending(state, onlinePlayerIds, now);
|
||||
LifecycleState current = clearLegacyPending(state);
|
||||
if (isInactive(current.game().tyrantId(), current.players(), now)) {
|
||||
return succession.succeed(
|
||||
current.asPersistentState(), Optional.empty(), onlinePlayerIds, now
|
||||
);
|
||||
return succession.succeed(current.asPersistentState(), Optional.empty());
|
||||
}
|
||||
if (isInactive(current.game().vigilanteId(), current.players(), now)) {
|
||||
current = rerollVigilante(
|
||||
current.asPersistentState(), current.game().vigilanteId(), onlinePlayerIds, now
|
||||
current.asPersistentState(), current.game().vigilanteId()
|
||||
);
|
||||
}
|
||||
current = rerollExpiredTyrant(current, onlinePlayerIds, now);
|
||||
if (current.game().tyrantId().isEmpty() && current.game().pendingTyrant().isEmpty()) {
|
||||
current = selectTyrant(current, Optional.empty(), onlinePlayerIds, now);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
public LifecycleState rerollVigilante(
|
||||
PersistentState state,
|
||||
Optional<UUID> previousVigilante,
|
||||
Set<UUID> onlinePlayerIds,
|
||||
Instant now
|
||||
Optional<UUID> previousVigilante
|
||||
) {
|
||||
Map<UUID, PlayerState> players = clearFollowers(state.players(), previousVigilante);
|
||||
GameState game = copyRoles(
|
||||
@@ -73,70 +47,12 @@ public final class RoleMaintenanceService {
|
||||
return new LifecycleState(game, players);
|
||||
}
|
||||
|
||||
private LifecycleState confirmPending(
|
||||
PersistentState state,
|
||||
Set<UUID> onlinePlayerIds,
|
||||
Instant now
|
||||
) {
|
||||
private static LifecycleState clearLegacyPending(PersistentState state) {
|
||||
GameState game = state.game();
|
||||
Optional<UUID> tyrant = game.tyrantId();
|
||||
Optional<UUID> vigilante = game.vigilanteId();
|
||||
Optional<PendingSelection> pendingTyrant = game.pendingTyrant();
|
||||
Optional<PendingSelection> pendingVigilante = game.pendingVigilante();
|
||||
if (pendingTyrant.isPresent()) {
|
||||
PendingSelection pending = pendingTyrant.orElseThrow();
|
||||
if (!now.isAfter(pending.expiresAt())
|
||||
&& onlinePlayerIds.contains(pending.candidateId())
|
||||
&& eligibleAtConfirmation(state.players().get(pending.candidateId()), now)
|
||||
&& vigilante.filter(pending.candidateId()::equals).isEmpty()) {
|
||||
tyrant = Optional.of(pending.candidateId());
|
||||
pendingTyrant = Optional.empty();
|
||||
}
|
||||
}
|
||||
// Legacy pending Vigilante selections become arena vacancies.
|
||||
pendingVigilante = Optional.empty();
|
||||
return new LifecycleState(
|
||||
copyRoles(game, tyrant, vigilante, pendingTyrant, pendingVigilante),
|
||||
state.players()
|
||||
GameState cleared = copyRoles(
|
||||
game, game.tyrantId(), game.vigilanteId(), Optional.empty(), Optional.empty()
|
||||
);
|
||||
}
|
||||
|
||||
private LifecycleState rerollExpiredTyrant(
|
||||
LifecycleState state,
|
||||
Set<UUID> onlinePlayerIds,
|
||||
Instant now
|
||||
) {
|
||||
Optional<PendingSelection> pending = state.game().pendingTyrant();
|
||||
if (pending.isEmpty() || !now.isAfter(pending.orElseThrow().expiresAt())) {
|
||||
return state;
|
||||
}
|
||||
return selectTyrant(
|
||||
state,
|
||||
Optional.of(pending.orElseThrow().candidateId()),
|
||||
onlinePlayerIds,
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
private LifecycleState selectTyrant(
|
||||
LifecycleState state,
|
||||
Optional<UUID> previous,
|
||||
Set<UUID> onlinePlayerIds,
|
||||
Instant now
|
||||
) {
|
||||
Set<UUID> excluded = state.game().vigilanteId().map(Set::of).orElseGet(Set::of);
|
||||
Optional<UUID> selected = candidates.select(
|
||||
state.players(), excluded, previous, 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(), active, state.game().vigilanteId(), pending,
|
||||
state.game().pendingVigilante()
|
||||
);
|
||||
return new LifecycleState(game, state.players());
|
||||
return new LifecycleState(cleared, state.players());
|
||||
}
|
||||
|
||||
private boolean isInactive(
|
||||
@@ -153,10 +69,6 @@ public final class RoleMaintenanceService {
|
||||
&& !player.lastLogin().orElseThrow().plus(inactivity).isAfter(now);
|
||||
}
|
||||
|
||||
private static boolean eligibleAtConfirmation(PlayerState player, Instant now) {
|
||||
return player != null && player.optedOutUntil().isEmpty();
|
||||
}
|
||||
|
||||
private static Map<UUID, PlayerState> clearFollowers(
|
||||
Map<UUID, PlayerState> players,
|
||||
Optional<UUID> previousVigilante
|
||||
|
||||
@@ -1,40 +1,29 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class RoleMaintenanceTask implements Runnable {
|
||||
private final TyrantStateManager stateManager;
|
||||
private final RoleMaintenanceService maintenance;
|
||||
private final TyrantPresentation presentation;
|
||||
private final Server server;
|
||||
private final Clock clock;
|
||||
|
||||
public RoleMaintenanceTask(
|
||||
TyrantStateManager stateManager,
|
||||
RoleMaintenanceService maintenance,
|
||||
TyrantPresentation presentation,
|
||||
Server server,
|
||||
Clock clock
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.maintenance = maintenance;
|
||||
this.presentation = presentation;
|
||||
this.server = server;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
PersistentState before = stateManager.snapshot();
|
||||
Set<UUID> online = server.getOnlinePlayers().stream()
|
||||
.map(Player::getUniqueId)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
LifecycleState after = maintenance.maintain(before, online, clock.instant());
|
||||
LifecycleState after = maintenance.maintain(before, clock.instant());
|
||||
if (!after.asPersistentState().equals(before)) {
|
||||
stateManager.replaceState(after);
|
||||
stateManager.saveIfDirty();
|
||||
|
||||
@@ -31,15 +31,13 @@ public final class RoleRelinquishmentService {
|
||||
}
|
||||
if (state.game().tyrantId().filter(playerId::equals).isPresent()) {
|
||||
return new RelinquishmentResult(
|
||||
succession.succeed(state, Optional.empty(), onlinePlayerIds, now),
|
||||
succession.succeed(state, Optional.empty()),
|
||||
RelinquishmentStatus.RELINQUISHED
|
||||
);
|
||||
}
|
||||
if (state.game().vigilanteId().filter(playerId::equals).isPresent()) {
|
||||
return new RelinquishmentResult(
|
||||
maintenance.rerollVigilante(
|
||||
state, Optional.of(playerId), onlinePlayerIds, now
|
||||
),
|
||||
maintenance.rerollVigilante(state, Optional.of(playerId)),
|
||||
RelinquishmentStatus.RELINQUISHED
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
private FixerEffectController fixerEffects;
|
||||
private TyrantEffectController tyrantEffects;
|
||||
private OptOutGlowController optOutGlow;
|
||||
private VigilanteArenaController vigilanteArena;
|
||||
private RoleArenaController roleArena;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -38,26 +38,14 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
try {
|
||||
arenaLocations.location();
|
||||
} catch (IllegalArgumentException exception) {
|
||||
getLogger().log(Level.SEVERE, "Invalid Vigilante arena location", exception);
|
||||
getLogger().log(Level.SEVERE, "Invalid shared role 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();
|
||||
TyrantSuccessionService succession = new TyrantSuccessionService(
|
||||
candidateSelector,
|
||||
settings.candidateActivityWindow(),
|
||||
settings.pendingSelectionTimeout(),
|
||||
random
|
||||
);
|
||||
TyrantSuccessionService succession = new TyrantSuccessionService();
|
||||
RoleMaintenanceService maintenance = new RoleMaintenanceService(
|
||||
candidateSelector,
|
||||
succession,
|
||||
settings.roleInactivity(),
|
||||
settings.candidateActivityWindow(),
|
||||
settings.pendingSelectionTimeout(),
|
||||
random
|
||||
succession, settings.roleInactivity()
|
||||
);
|
||||
BukkitOnlinePlayerDirectory onlinePlayers = new BukkitOnlinePlayerDirectory();
|
||||
FollowerService followers = new FollowerService();
|
||||
@@ -154,10 +142,10 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
new PlayerJoinListener(stateManager, clock, tyrantPresentation),
|
||||
this
|
||||
);
|
||||
vigilanteArena = new VigilanteArenaController(
|
||||
roleArena = new RoleArenaController(
|
||||
stateManager, getServer(), arenaLocations, settings
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(vigilanteArena, this);
|
||||
getServer().getPluginManager().registerEvents(roleArena, this);
|
||||
getServer().getPluginManager().registerEvents(controlPanelRenderer, this);
|
||||
getServer().getPluginManager().registerEvents(vigilanteControlPanelRenderer, this);
|
||||
getServer().getPluginManager().registerEvents(
|
||||
@@ -180,14 +168,14 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new TyrantDeathListener(
|
||||
stateManager, succession, tyrantPresentation, getServer(), clock
|
||||
stateManager, succession, tyrantPresentation, getServer()
|
||||
),
|
||||
this
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new VigilanteDeathListener(
|
||||
stateManager, new TyrantProgressionService(), maintenance,
|
||||
getServer(), clock
|
||||
getServer()
|
||||
),
|
||||
this
|
||||
);
|
||||
@@ -238,12 +226,12 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
this
|
||||
);
|
||||
long maintenanceTicks = Math.max(
|
||||
1L, Math.multiplyExact(settings.selectionRetryInterval().toSeconds(), 20L)
|
||||
1L, Math.multiplyExact(settings.roleMaintenanceInterval().toSeconds(), 20L)
|
||||
);
|
||||
getServer().getScheduler().runTaskTimer(
|
||||
this,
|
||||
new RoleMaintenanceTask(
|
||||
stateManager, maintenance, tyrantPresentation, getServer(), clock
|
||||
stateManager, maintenance, tyrantPresentation, clock
|
||||
),
|
||||
maintenanceTicks,
|
||||
maintenanceTicks
|
||||
@@ -253,7 +241,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, roleArena, 1L, 10L);
|
||||
getServer().getScheduler().runTaskTimer(
|
||||
this,
|
||||
new AbilityItemRefreshTask(
|
||||
@@ -283,8 +271,8 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (vigilanteArena != null) {
|
||||
vigilanteArena.close();
|
||||
if (roleArena != null) {
|
||||
roleArena.close();
|
||||
}
|
||||
if (optOutGlow != null) {
|
||||
optOutGlow.clearAll();
|
||||
|
||||
@@ -143,7 +143,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
|
||||
private void arena(CommandSender sender, String[] arguments) {
|
||||
if (arguments.length == 1 || arguments[1].equalsIgnoreCase("status")) {
|
||||
sender.sendMessage("Vigilante arena: " + arenaLocations.location()
|
||||
sender.sendMessage("Shared role arena: " + arenaLocations.location()
|
||||
.map(ArenaLocation::display).orElse("not configured"));
|
||||
return;
|
||||
}
|
||||
@@ -162,7 +162,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
location.getYaw(), location.getPitch()
|
||||
);
|
||||
arenaLocations.set(arena);
|
||||
sender.sendMessage(ChatColor.GREEN + "Vigilante arena set to " + arena.display() + ".");
|
||||
sender.sendMessage(ChatColor.GREEN + "Shared role arena set to " + arena.display() + ".");
|
||||
}
|
||||
|
||||
private void showStatus(CommandSender sender) {
|
||||
@@ -176,7 +176,7 @@ 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()
|
||||
sender.sendMessage("Shared role arena: " + arenaLocations.location()
|
||||
.map(ArenaLocation::display).orElse("not configured"));
|
||||
for (PlayerState player : stateManager.players().values()) {
|
||||
boolean optedOut = player.optedOutUntil().isPresent();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class TyrantArenaSuccessionService {
|
||||
public LifecycleState claim(PersistentState state, UUID winnerId) {
|
||||
PlayerState winner = state.players().get(winnerId);
|
||||
if (!RoleArenaPolicy.isEligible(state.game(), ArenaRole.TYRANT, winner)) {
|
||||
throw new IllegalStateException("player is not eligible to claim the Tyrant role");
|
||||
}
|
||||
GameState game = state.game();
|
||||
GameState assigned = new GameState(
|
||||
game.lifecycle(), Optional.of(winnerId), Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), game.pausedAt(), game.accumulatedPausedTime(),
|
||||
0, 1, Set.of()
|
||||
);
|
||||
return new LifecycleState(assigned, state.players());
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -18,20 +15,17 @@ public final class TyrantDeathListener implements Listener {
|
||||
private final TyrantSuccessionService succession;
|
||||
private final TyrantPresentation presentation;
|
||||
private final Server server;
|
||||
private final Clock clock;
|
||||
|
||||
public TyrantDeathListener(
|
||||
TyrantStateManager stateManager,
|
||||
TyrantSuccessionService succession,
|
||||
TyrantPresentation presentation,
|
||||
Server server,
|
||||
Clock clock
|
||||
Server server
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.succession = succession;
|
||||
this.presentation = presentation;
|
||||
this.server = server;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
@@ -44,22 +38,18 @@ public final class TyrantDeathListener implements Listener {
|
||||
}
|
||||
Optional<UUID> killerId = Optional.ofNullable(event.getEntity().getKiller())
|
||||
.map(Player::getUniqueId);
|
||||
Set<UUID> online = server.getOnlinePlayers().stream()
|
||||
.map(Player::getUniqueId)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
LifecycleState succeeded = succession.succeed(
|
||||
stateManager.snapshot(), killerId, online, clock.instant()
|
||||
stateManager.snapshot(), killerId
|
||||
);
|
||||
stateManager.replaceState(succeeded);
|
||||
stateManager.saveIfDirty();
|
||||
presentation.reconcile(succeeded.game().tyrantId());
|
||||
String successor = succeeded.game().tyrantId()
|
||||
.map(id -> {
|
||||
Player player = server.getPlayer(id);
|
||||
return player == null ? id.toString() : player.getName();
|
||||
})
|
||||
.orElse("pending selection");
|
||||
String message = ChatColor.RED + successor + " is the new Tyrant!";
|
||||
String message = succeeded.game().tyrantId().map(id -> {
|
||||
Player player = server.getPlayer(id);
|
||||
String successor = player == null ? id.toString() : player.getName();
|
||||
return ChatColor.RED + successor + " is the new Tyrant!";
|
||||
}).orElse(ChatColor.RED
|
||||
+ "The Tyrant role is vacant; the red arena challenge is now open.");
|
||||
server.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +1,26 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
public final class TyrantSuccessionService {
|
||||
private final RoleCandidateSelector candidates;
|
||||
private final Duration activityWindow;
|
||||
private final Duration pendingTimeout;
|
||||
private final RandomGenerator random;
|
||||
|
||||
public TyrantSuccessionService(
|
||||
RoleCandidateSelector candidates,
|
||||
Duration activityWindow,
|
||||
Duration pendingTimeout,
|
||||
RandomGenerator random
|
||||
) {
|
||||
this.candidates = candidates;
|
||||
this.activityWindow = activityWindow;
|
||||
this.pendingTimeout = pendingTimeout;
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
public LifecycleState succeed(
|
||||
PersistentState state,
|
||||
Optional<UUID> killerId,
|
||||
Set<UUID> onlinePlayerIds,
|
||||
Instant now
|
||||
Optional<UUID> killerId
|
||||
) {
|
||||
if (state.game().lifecycle() != GameLifecycle.RUNNING
|
||||
|| state.game().tyrantId().isEmpty()) {
|
||||
throw new IllegalStateException("a running Tyrant reign is required");
|
||||
}
|
||||
UUID formerTyrant = state.game().tyrantId().orElseThrow();
|
||||
Map<UUID, PlayerState> clearedPlayers = clearAssignments(state.players());
|
||||
Optional<UUID> selectedTyrant = killerId.isPresent()
|
||||
? killerId
|
||||
: candidates.select(
|
||||
clearedPlayers,
|
||||
Set.of(),
|
||||
Optional.of(formerTyrant),
|
||||
now,
|
||||
activityWindow,
|
||||
random
|
||||
);
|
||||
Optional<UUID> activeTyrant = selectedTyrant
|
||||
.filter(playerId -> killerId.isPresent() || onlinePlayerIds.contains(playerId));
|
||||
Optional<PendingSelection> pendingTyrant = selectedTyrant
|
||||
.filter(playerId -> activeTyrant.isEmpty())
|
||||
.map(playerId -> new PendingSelection(playerId, now.plus(pendingTimeout)));
|
||||
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING,
|
||||
activeTyrant,
|
||||
killerId,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
pendingTyrant,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
state.game().accumulatedPausedTime(),
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ 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)) {
|
||||
if (!RoleArenaPolicy.isEligible(
|
||||
state.game(), ArenaRole.VIGILANTE, winner
|
||||
)) {
|
||||
throw new IllegalStateException("player is not eligible to claim the Vigilante role");
|
||||
}
|
||||
GameState game = state.game();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Server;
|
||||
@@ -17,20 +15,17 @@ public final class VigilanteDeathListener implements Listener {
|
||||
private final TyrantProgressionService progression;
|
||||
private final RoleMaintenanceService maintenance;
|
||||
private final Server server;
|
||||
private final Clock clock;
|
||||
|
||||
public VigilanteDeathListener(
|
||||
TyrantStateManager stateManager,
|
||||
TyrantProgressionService progression,
|
||||
RoleMaintenanceService maintenance,
|
||||
Server server,
|
||||
Clock clock
|
||||
Server server
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.progression = progression;
|
||||
this.maintenance = maintenance;
|
||||
this.server = server;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
@@ -47,11 +42,8 @@ public final class VigilanteDeathListener implements Listener {
|
||||
PersistentState withProgress = new PersistentState(
|
||||
progressed, stateManager.players()
|
||||
);
|
||||
Set<UUID> online = server.getOnlinePlayers().stream()
|
||||
.map(Player::getUniqueId)
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
LifecycleState rerolled = maintenance.rerollVigilante(
|
||||
withProgress, Optional.of(victimId), online, clock.instant()
|
||||
withProgress, Optional.of(victimId)
|
||||
);
|
||||
stateManager.replaceState(rerolled);
|
||||
stateManager.saveIfDirty();
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
tyrant-range-blocks: 50
|
||||
follower-range-blocks: 50
|
||||
|
||||
# Vigilante arena and boss
|
||||
# Shared Tyrant and Vigilante arena and boss
|
||||
vigilante-arena-radius-blocks: 10
|
||||
vigilante-boss-health: 80
|
||||
vigilante-boss-damage: 8
|
||||
vigilante-boss-armor: 10
|
||||
# Set with /tyrantadmin arena set
|
||||
|
||||
# Participation, inactivity, and selection
|
||||
# Participation and role inactivity
|
||||
opt-out-seconds: 604800
|
||||
role-inactivity-seconds: 172800
|
||||
candidate-activity-window-seconds: 86400
|
||||
pending-selection-timeout-seconds: 86400
|
||||
selection-retry-seconds: 60
|
||||
role-maintenance-seconds: 60
|
||||
freeze-timers-while-paused: true
|
||||
|
||||
# Assassin
|
||||
|
||||
@@ -20,6 +20,7 @@ final class DefaultConfigurationTest {
|
||||
PluginSettings settings = PluginSettings.from(values);
|
||||
assertEquals(50.0, settings.tyrantRangeBlocks());
|
||||
assertEquals(Duration.ofDays(7), settings.optOutDuration());
|
||||
assertEquals(Duration.ofMinutes(1), settings.roleMaintenanceInterval());
|
||||
assertEquals("FISHING_ROD", settings.tamerItem().material());
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ final class PluginSettingsTest {
|
||||
assertEquals(50.0, settings.followerRangeBlocks());
|
||||
assertEquals(Duration.ofDays(7), settings.optOutDuration());
|
||||
assertEquals(Duration.ofHours(48), settings.roleInactivity());
|
||||
assertEquals(Duration.ofHours(24), settings.candidateActivityWindow());
|
||||
assertEquals(Duration.ofHours(24), settings.pendingSelectionTimeout());
|
||||
assertEquals(Duration.ofMinutes(1), settings.roleMaintenanceInterval());
|
||||
assertEquals(Duration.ofHours(1), settings.assassinCooldown());
|
||||
assertEquals(Duration.ofMinutes(10), settings.assassinInvisibilityDuration());
|
||||
assertEquals(Duration.ofHours(1), settings.fixerCooldown());
|
||||
@@ -30,12 +29,18 @@ final class PluginSettingsTest {
|
||||
assertEquals(10.0, settings.vigilanteBossArmor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsLegacySelectionRetryAsMaintenanceInterval() {
|
||||
PluginSettings settings = PluginSettings.from(Map.of("selection-retry-seconds", 30));
|
||||
|
||||
assertEquals(Duration.ofSeconds(30), settings.roleMaintenanceInterval());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedDefaultsCoverAbilitiesItemsMobsAndMessages() {
|
||||
PluginSettings settings = PluginSettings.from(Map.of());
|
||||
|
||||
assertEquals(Duration.ofMinutes(1), settings.selectionRetryInterval());
|
||||
assertEquals(Duration.ofSeconds(60), settings.assassinDoubleJumpCooldown());
|
||||
assertEquals(Duration.ofSeconds(60), settings.assassinDoubleJumpCooldown());
|
||||
assertEquals(Duration.ofSeconds(15), settings.assassinSpeedDuration());
|
||||
assertEquals(Duration.ofSeconds(20), settings.assassinWeaknessDuration());
|
||||
assertEquals(3, settings.assassinWeaknessLevel());
|
||||
@@ -71,6 +76,10 @@ final class PluginSettingsTest {
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettings.from(Map.of("role-inactivity-seconds", -1))
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettings.from(Map.of("role-maintenance-seconds", 0))
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettings.from(Map.of("vigilante-arena-radius-blocks", 0))
|
||||
|
||||
+40
-3
@@ -1,5 +1,7 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -12,15 +14,23 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class VigilanteArenaControllerTest {
|
||||
final class RoleArenaControllerTest {
|
||||
private static final UUID TYRANT = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
private static final UUID PLAYER = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
@Test
|
||||
void boundaryColorsIdentifyTheOpenRole() {
|
||||
assertEquals(Color.RED, RoleArenaController.boundaryColor(ArenaRole.TYRANT));
|
||||
assertEquals(Color.BLUE, RoleArenaController.boundaryColor(ArenaRole.VIGILANTE));
|
||||
assertNull(RoleArenaController.boundaryColor(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void optedInPlayerJoiningDuringVacancyReceivesArenaCoordinates() {
|
||||
Player player = mock(Player.class);
|
||||
@@ -32,7 +42,7 @@ final class VigilanteArenaControllerTest {
|
||||
when(locations.location()).thenReturn(Optional.of(
|
||||
new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F)
|
||||
));
|
||||
VigilanteArenaController controller = new VigilanteArenaController(
|
||||
RoleArenaController controller = new RoleArenaController(
|
||||
manager, mock(Server.class), locations, PluginSettings.from(Map.of())
|
||||
);
|
||||
|
||||
@@ -41,6 +51,26 @@ final class VigilanteArenaControllerTest {
|
||||
verify(player).sendMessage(contains("world at 10, 64, -5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void optedInPlayerIsNotifiedOfTyrantVacancy() {
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(PLAYER);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(tyrantVacancy());
|
||||
when(manager.players()).thenReturn(Map.of(PLAYER, PlayerState.newPlayer(PLAYER, "Player")));
|
||||
ArenaLocationStore locations = mock(ArenaLocationStore.class);
|
||||
when(locations.location()).thenReturn(Optional.of(
|
||||
new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F)
|
||||
));
|
||||
RoleArenaController controller = new RoleArenaController(
|
||||
manager, mock(Server.class), locations, PluginSettings.from(Map.of())
|
||||
);
|
||||
|
||||
controller.onJoin(mockJoin(player));
|
||||
|
||||
verify(player).sendMessage(contains("Tyrant position is open"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void optedOutPlayerDoesNotReceiveVacancyNotice() {
|
||||
Player player = mock(Player.class);
|
||||
@@ -56,7 +86,7 @@ final class VigilanteArenaControllerTest {
|
||||
when(locations.location()).thenReturn(Optional.of(
|
||||
new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F)
|
||||
));
|
||||
VigilanteArenaController controller = new VigilanteArenaController(
|
||||
RoleArenaController controller = new RoleArenaController(
|
||||
manager, mock(Server.class), locations, PluginSettings.from(Map.of())
|
||||
);
|
||||
|
||||
@@ -71,6 +101,13 @@ final class VigilanteArenaControllerTest {
|
||||
return event;
|
||||
}
|
||||
|
||||
private static GameState tyrantVacancy() {
|
||||
return new GameState(
|
||||
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Duration.ZERO, 0, 1, Set.of()
|
||||
);
|
||||
}
|
||||
|
||||
private static GameState openGame() {
|
||||
return new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(), Optional.empty(),
|
||||
@@ -0,0 +1,78 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class RoleArenaPolicyTest {
|
||||
private static final UUID TYRANT = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
private static final UUID PLAYER = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
@Test
|
||||
void tyrantVacancyHasPriorityOverVigilanteVacancy() {
|
||||
assertEquals(Optional.of(ArenaRole.TYRANT), RoleArenaPolicy.openRole(game(
|
||||
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(), Optional.empty()
|
||||
)));
|
||||
assertEquals(Optional.of(ArenaRole.VIGILANTE), RoleArenaPolicy.openRole(game(
|
||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(), Optional.empty()
|
||||
)));
|
||||
assertEquals(Optional.empty(), RoleArenaPolicy.openRole(game(
|
||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.of(PLAYER), Optional.empty()
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pausedGameAndLegacyPendingTyrantDoNotOpenFight() {
|
||||
assertEquals(Optional.empty(), RoleArenaPolicy.openRole(game(
|
||||
GameLifecycle.PAUSED, Optional.empty(), Optional.empty(), Optional.empty()
|
||||
)));
|
||||
assertEquals(Optional.empty(), RoleArenaPolicy.openRole(game(
|
||||
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(),
|
||||
Optional.of(new PendingSelection(PLAYER, Instant.parse("2026-08-24T00:00:00Z")))
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeTyrantCannotChallengeAndOptedOutPlayersRemainIneligible() {
|
||||
GameState vigilanteFight = game(
|
||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(), Optional.empty()
|
||||
);
|
||||
GameState tyrantFight = game(
|
||||
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(), Optional.empty()
|
||||
);
|
||||
PlayerState eligible = PlayerState.newPlayer(PLAYER, "Player");
|
||||
PlayerState optedOut = new PlayerState(
|
||||
PLAYER, "Player", Optional.empty(), Optional.of(Instant.parse("2026-08-24T12:00:00Z")),
|
||||
TyrantClass.NONE, Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
||||
);
|
||||
|
||||
assertTrue(RoleArenaPolicy.isEligible(vigilanteFight, ArenaRole.VIGILANTE, eligible));
|
||||
assertTrue(RoleArenaPolicy.isEligible(tyrantFight, ArenaRole.TYRANT, eligible));
|
||||
assertFalse(RoleArenaPolicy.isEligible(vigilanteFight, ArenaRole.VIGILANTE, optedOut));
|
||||
assertFalse(RoleArenaPolicy.isEligible(
|
||||
vigilanteFight, ArenaRole.VIGILANTE, PlayerState.newPlayer(TYRANT, "Tyrant")
|
||||
));
|
||||
}
|
||||
|
||||
private static GameState game(
|
||||
GameLifecycle lifecycle,
|
||||
Optional<UUID> tyrant,
|
||||
Optional<UUID> vigilante,
|
||||
Optional<PendingSelection> pendingTyrant
|
||||
) {
|
||||
return new GameState(
|
||||
lifecycle, tyrant, vigilante, pendingTyrant, Optional.empty(),
|
||||
lifecycle == GameLifecycle.PAUSED
|
||||
? Optional.of(Instant.parse("2026-08-23T12:00:00Z")) : Optional.empty(),
|
||||
Duration.ZERO, 0, 0, Set.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.random.RandomGenerator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class RoleCandidateSelectorTest {
|
||||
@Test
|
||||
void selectsOnlyRecentlyActiveEligibleNonExcludedPlayer() {
|
||||
Instant now = Instant.parse("2026-08-14T12:00:00Z");
|
||||
UUID tyrant = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID candidate = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
UUID stale = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
Map<UUID, PlayerState> players = Map.of(
|
||||
tyrant, withLogin(PlayerState.newPlayer(tyrant, "Tyrant"), now),
|
||||
candidate, withLogin(PlayerState.newPlayer(candidate, "Candidate"), now.minusSeconds(60)),
|
||||
stale, withLogin(PlayerState.newPlayer(stale, "Stale"), now.minus(Duration.ofHours(25)))
|
||||
);
|
||||
|
||||
Optional<UUID> selected = new RoleCandidateSelector().select(
|
||||
players,
|
||||
Set.of(tyrant),
|
||||
Optional.empty(),
|
||||
now,
|
||||
Duration.ofHours(24),
|
||||
RandomGenerator.of("L64X128MixRandom")
|
||||
);
|
||||
|
||||
assertEquals(Optional.of(candidate), selected);
|
||||
}
|
||||
|
||||
private static PlayerState withLogin(PlayerState player, Instant login) {
|
||||
return new PlayerState(
|
||||
player.playerId(), player.latestName(), Optional.of(login), player.optedOutUntil(),
|
||||
player.tyrantClass(), player.followerOf(), player.cooldownEnds(),
|
||||
player.readyAbilityItems(), player.capturedMobs()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.random.RandomGenerator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class RoleMaintenanceServiceTest {
|
||||
@@ -18,19 +17,25 @@ final class RoleMaintenanceServiceTest {
|
||||
private static final UUID OTHER = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||
private static final UUID FOLLOWER = UUID.fromString("55555555-5555-5555-5555-555555555555");
|
||||
private static final Instant NOW = Instant.parse("2026-08-14T12:00:00Z");
|
||||
private final RandomGenerator random = RandomGenerator.of("L64X128MixRandom");
|
||||
private final RoleCandidateSelector selector = new RoleCandidateSelector();
|
||||
private final RoleMaintenanceService service = new RoleMaintenanceService(
|
||||
selector,
|
||||
new TyrantSuccessionService(
|
||||
selector, Duration.ofHours(24), Duration.ofHours(24), random
|
||||
),
|
||||
Duration.ofHours(48),
|
||||
Duration.ofHours(24),
|
||||
Duration.ofHours(24),
|
||||
random
|
||||
new TyrantSuccessionService(), Duration.ofHours(48)
|
||||
);
|
||||
|
||||
@Test
|
||||
void legacyPendingTyrantIsClearedIntoArenaVacancy() {
|
||||
GameState game = game(
|
||||
Optional.empty(), Optional.empty(),
|
||||
Optional.of(new PendingSelection(CANDIDATE, NOW.plusSeconds(60))),
|
||||
Optional.empty()
|
||||
);
|
||||
PersistentState state = new PersistentState(game, players(active(CANDIDATE)));
|
||||
|
||||
LifecycleState maintained = service.maintain(state, NOW);
|
||||
|
||||
assertEquals(Optional.empty(), maintained.game().tyrantId());
|
||||
assertEquals(Optional.empty(), maintained.game().pendingTyrant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyPendingVigilanteIsClearedForArenaSuccession() {
|
||||
GameState game = game(
|
||||
@@ -41,7 +46,7 @@ final class RoleMaintenanceServiceTest {
|
||||
active(TYRANT), active(CANDIDATE)
|
||||
));
|
||||
|
||||
LifecycleState maintained = service.maintain(state, Set.of(TYRANT, CANDIDATE), NOW);
|
||||
LifecycleState maintained = service.maintain(state, NOW);
|
||||
|
||||
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), maintained.game().pendingVigilante());
|
||||
@@ -57,7 +62,7 @@ final class RoleMaintenanceServiceTest {
|
||||
active(TYRANT), active(CANDIDATE), active(OTHER)
|
||||
));
|
||||
|
||||
LifecycleState maintained = service.maintain(state, Set.of(TYRANT), NOW);
|
||||
LifecycleState maintained = service.maintain(state, NOW);
|
||||
|
||||
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), maintained.game().pendingVigilante());
|
||||
@@ -78,7 +83,7 @@ final class RoleMaintenanceServiceTest {
|
||||
FOLLOWER, follower, OTHER, active(OTHER)
|
||||
));
|
||||
|
||||
LifecycleState maintained = service.maintain(state, Set.of(TYRANT, OTHER), NOW);
|
||||
LifecycleState maintained = service.maintain(state, NOW);
|
||||
|
||||
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), maintained.players().get(FOLLOWER).followerOf());
|
||||
@@ -86,7 +91,7 @@ final class RoleMaintenanceServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void inactiveTyrantEndsReignAndUsesNormalSuccession() {
|
||||
void inactiveTyrantEndsReignAndOpensArena() {
|
||||
PlayerState staleTyrant = withLogin(
|
||||
active(TYRANT), NOW.minus(Duration.ofHours(48))
|
||||
);
|
||||
@@ -104,8 +109,10 @@ final class RoleMaintenanceServiceTest {
|
||||
TYRANT, staleTyrant, VIGILANTE, active(VIGILANTE), OTHER, successor
|
||||
));
|
||||
|
||||
LifecycleState maintained = service.maintain(state, Set.of(OTHER), NOW);
|
||||
LifecycleState maintained = service.maintain(state, NOW);
|
||||
|
||||
assertEquals(Optional.empty(), maintained.game().tyrantId());
|
||||
assertEquals(Optional.empty(), maintained.game().pendingTyrant());
|
||||
assertEquals(0, maintained.game().tyrantLevel());
|
||||
assertEquals(1, maintained.game().unspentChoices());
|
||||
assertEquals(Set.of(), maintained.game().purchases());
|
||||
|
||||
@@ -8,7 +8,6 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.random.RandomGenerator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class RoleRelinquishmentServiceTest {
|
||||
@@ -18,14 +17,9 @@ final class RoleRelinquishmentServiceTest {
|
||||
UUID vigilante = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
UUID candidate = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
Instant now = Instant.parse("2026-08-14T12:00:00Z");
|
||||
RoleCandidateSelector selector = new RoleCandidateSelector();
|
||||
RandomGenerator random = RandomGenerator.of("L64X128MixRandom");
|
||||
TyrantSuccessionService succession = new TyrantSuccessionService(
|
||||
selector, Duration.ofHours(24), Duration.ofHours(24), random
|
||||
);
|
||||
TyrantSuccessionService succession = new TyrantSuccessionService();
|
||||
RoleMaintenanceService maintenance = new RoleMaintenanceService(
|
||||
selector, succession, Duration.ofHours(48), Duration.ofHours(24),
|
||||
Duration.ofHours(24), random
|
||||
succession, Duration.ofHours(48)
|
||||
);
|
||||
RoleRelinquishmentService service = new RoleRelinquishmentService(
|
||||
succession, maintenance
|
||||
@@ -53,6 +47,38 @@ final class RoleRelinquishmentServiceTest {
|
||||
assertEquals(Optional.empty(), result.state().game().pendingVigilante());
|
||||
}
|
||||
|
||||
@Test
|
||||
void tyrantRelinquishmentOpensTyrantArenaAndClearsReign() {
|
||||
UUID tyrant = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID vigilante = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
Instant now = Instant.parse("2026-08-14T12:00:00Z");
|
||||
TyrantSuccessionService succession = new TyrantSuccessionService();
|
||||
RoleRelinquishmentService service = new RoleRelinquishmentService(
|
||||
succession, new RoleMaintenanceService(succession, Duration.ofHours(48))
|
||||
);
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrant), Optional.of(vigilante),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
4, 0, Set.of(TyrantUnlock.STRENGTH)
|
||||
);
|
||||
|
||||
RelinquishmentResult result = service.relinquish(
|
||||
new PersistentState(game, Map.of(
|
||||
tyrant, active(tyrant, now), vigilante, active(vigilante, now)
|
||||
)),
|
||||
tyrant,
|
||||
Set.of(tyrant, vigilante),
|
||||
now
|
||||
);
|
||||
|
||||
assertEquals(RelinquishmentStatus.RELINQUISHED, result.status());
|
||||
assertEquals(Optional.empty(), result.state().game().tyrantId());
|
||||
assertEquals(Optional.empty(), result.state().game().pendingTyrant());
|
||||
assertEquals(Optional.empty(), result.state().game().vigilanteId());
|
||||
assertEquals(0, result.state().game().tyrantLevel());
|
||||
assertEquals(1, result.state().game().unspentChoices());
|
||||
}
|
||||
|
||||
private static PlayerState active(UUID id, Instant now) {
|
||||
PlayerState player = PlayerState.newPlayer(id, id.toString());
|
||||
return new PlayerState(
|
||||
|
||||
@@ -83,7 +83,7 @@ final class TyrantAdminCommandTest {
|
||||
location.worldName().equals("arena_world")
|
||||
&& location.x() == 12.5 && location.y() == 70.0 && location.z() == -4.5
|
||||
));
|
||||
verify(sender).sendMessage(contains("Vigilante arena set"));
|
||||
verify(sender).sendMessage(contains("Shared role arena set"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TyrantArenaSuccessionServiceTest {
|
||||
@Test
|
||||
void eligibleBossWinnerClaimsVacantTyrantRoleWithFreshReign() {
|
||||
UUID winner = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Duration.ZERO, 0, 1, Set.of()
|
||||
);
|
||||
PersistentState before = new PersistentState(
|
||||
game, Map.of(winner, PlayerState.newPlayer(winner, "Winner"))
|
||||
);
|
||||
|
||||
LifecycleState claimed = new TyrantArenaSuccessionService().claim(before, winner);
|
||||
|
||||
assertEquals(Optional.of(winner), claimed.game().tyrantId());
|
||||
assertEquals(Optional.empty(), claimed.game().vigilanteId());
|
||||
assertEquals(0, claimed.game().tyrantLevel());
|
||||
assertEquals(1, claimed.game().unspentChoices());
|
||||
assertEquals(Set.of(), claimed.game().purchases());
|
||||
assertEquals(
|
||||
Optional.of(ArenaRole.VIGILANTE),
|
||||
RoleArenaPolicy.openRole(claimed.game())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -42,7 +38,7 @@ final class TyrantDeathListenerTest {
|
||||
when(manager.game()).thenReturn(game);
|
||||
when(manager.snapshot()).thenReturn(before);
|
||||
TyrantSuccessionService succession = mock(TyrantSuccessionService.class);
|
||||
when(succession.succeed(eq(before), eq(Optional.of(killerId)), any(), any()))
|
||||
when(succession.succeed(eq(before), eq(Optional.of(killerId))))
|
||||
.thenReturn(succeeded);
|
||||
TyrantPresentation presentation = mock(TyrantPresentation.class);
|
||||
Server server = mock(Server.class);
|
||||
@@ -58,8 +54,7 @@ final class TyrantDeathListenerTest {
|
||||
manager,
|
||||
succession,
|
||||
presentation,
|
||||
server,
|
||||
Clock.fixed(Instant.parse("2026-08-14T12:00:00Z"), ZoneOffset.UTC)
|
||||
server
|
||||
);
|
||||
|
||||
listener.onDeath(event);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -9,7 +8,6 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.random.RandomGenerator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TyrantSuccessionServiceTest {
|
||||
@@ -18,19 +16,14 @@ final class TyrantSuccessionServiceTest {
|
||||
private static final UUID KILLER = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
private static final UUID OTHER = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||
private static final Instant NOW = Instant.parse("2026-08-14T12:00:00Z");
|
||||
private final TyrantSuccessionService service = new TyrantSuccessionService(
|
||||
new RoleCandidateSelector(),
|
||||
Duration.ofHours(24),
|
||||
Duration.ofHours(24),
|
||||
RandomGenerator.of("L64X128MixRandom")
|
||||
);
|
||||
private final TyrantSuccessionService service = new TyrantSuccessionService();
|
||||
|
||||
@Test
|
||||
void playerKillerBecomesTyrantAndEntireReignIsReset() {
|
||||
PersistentState before = progressedState();
|
||||
|
||||
LifecycleState after = service.succeed(
|
||||
before, Optional.of(KILLER), Set.of(KILLER, OTHER), NOW
|
||||
before, Optional.of(KILLER)
|
||||
);
|
||||
|
||||
assertEquals(Optional.of(KILLER), after.game().tyrantId());
|
||||
@@ -39,6 +32,9 @@ final class TyrantSuccessionServiceTest {
|
||||
assertEquals(Set.of(), after.game().purchases());
|
||||
assertEquals(Optional.empty(), after.game().vigilanteId());
|
||||
assertEquals(Optional.empty(), after.game().pendingVigilante());
|
||||
assertEquals(
|
||||
Optional.of(ArenaRole.VIGILANTE), RoleArenaPolicy.openRole(after.game())
|
||||
);
|
||||
for (PlayerState player : after.players().values()) {
|
||||
assertEquals(TyrantClass.NONE, player.tyrantClass());
|
||||
assertEquals(Optional.empty(), player.followerOf());
|
||||
@@ -47,15 +43,14 @@ final class TyrantSuccessionServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void environmentalDeathRandomlySelectsRecentPlayerInsteadOfFormerTyrant() {
|
||||
void environmentalDeathOpensTyrantArenaWithoutPendingSelection() {
|
||||
PersistentState before = progressedState();
|
||||
|
||||
LifecycleState after = service.succeed(before, Optional.empty(), Set.of(OTHER), NOW);
|
||||
LifecycleState after = service.succeed(before, Optional.empty());
|
||||
|
||||
UUID selected = after.game().tyrantId().orElseGet(() ->
|
||||
after.game().pendingTyrant().orElseThrow().candidateId()
|
||||
);
|
||||
assertNotEquals(OLD_TYRANT, selected);
|
||||
assertEquals(Optional.empty(), after.game().tyrantId());
|
||||
assertEquals(Optional.empty(), after.game().pendingTyrant());
|
||||
assertEquals(Optional.empty(), after.game().vigilanteId());
|
||||
}
|
||||
|
||||
private static PersistentState progressedState() {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class VigilanteArenaPolicyTest {
|
||||
private static final UUID TYRANT = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
private static final UUID PLAYER = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
@Test
|
||||
void opensOnlyForRunningGameWithTyrantAndVacantVigilante() {
|
||||
assertTrue(VigilanteArenaPolicy.isOpen(game(
|
||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty()
|
||||
)));
|
||||
assertFalse(VigilanteArenaPolicy.isOpen(game(
|
||||
GameLifecycle.PAUSED, Optional.of(TYRANT), Optional.empty()
|
||||
)));
|
||||
assertFalse(VigilanteArenaPolicy.isOpen(game(
|
||||
GameLifecycle.RUNNING, Optional.empty(), Optional.empty()
|
||||
)));
|
||||
assertFalse(VigilanteArenaPolicy.isOpen(game(
|
||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.of(PLAYER)
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyOptedInNonTyrantMayChallenge() {
|
||||
GameState game = game(GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty());
|
||||
PlayerState eligible = PlayerState.newPlayer(PLAYER, "Player");
|
||||
PlayerState optedOut = new PlayerState(
|
||||
PLAYER, "Player", Optional.empty(), Optional.of(Instant.parse("2026-08-24T12:00:00Z")),
|
||||
TyrantClass.NONE, Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
||||
);
|
||||
|
||||
assertTrue(VigilanteArenaPolicy.isEligible(game, eligible));
|
||||
assertFalse(VigilanteArenaPolicy.isEligible(game, optedOut));
|
||||
assertFalse(VigilanteArenaPolicy.isEligible(game,
|
||||
PlayerState.newPlayer(TYRANT, "Tyrant")));
|
||||
}
|
||||
|
||||
private static GameState game(
|
||||
GameLifecycle lifecycle,
|
||||
Optional<UUID> tyrant,
|
||||
Optional<UUID> vigilante
|
||||
) {
|
||||
return new GameState(
|
||||
lifecycle, tyrant, vigilante, Optional.empty(), Optional.empty(),
|
||||
lifecycle == GameLifecycle.PAUSED
|
||||
? Optional.of(Instant.parse("2026-08-23T12:00:00Z")) : Optional.empty(),
|
||||
Duration.ZERO, 0, 0, Set.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user