feat(tyrant): summon arena bosses only near players
This commit is contained in:
@@ -12,14 +12,29 @@ final class RoleArenaBarrier {
|
||||
private final Map<Location, BlockData> replaced = new LinkedHashMap<>();
|
||||
private boolean built;
|
||||
|
||||
void build(
|
||||
boolean build(
|
||||
World world,
|
||||
ArenaLocation center,
|
||||
double radius,
|
||||
int wallHeight
|
||||
) {
|
||||
if (built) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
restorePending();
|
||||
if (!replaced.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int minX = ((int) Math.floor(center.x() - radius)) >> 4;
|
||||
int maxX = ((int) Math.floor(center.x() + radius)) >> 4;
|
||||
int minZ = ((int) Math.floor(center.z() - radius)) >> 4;
|
||||
int maxZ = ((int) Math.floor(center.z() + radius)) >> 4;
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
if (!world.isChunkLoaded(x, z)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
built = true;
|
||||
int samples = Math.max(64, (int) Math.ceil(2.0 * Math.PI * radius * 2.0));
|
||||
@@ -38,18 +53,33 @@ final class RoleArenaBarrier {
|
||||
block.setType(Material.BARRIER, false);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
for (Map.Entry<Location, BlockData> entry : replaced.entrySet()) {
|
||||
built = false;
|
||||
restorePending();
|
||||
}
|
||||
|
||||
void restorePending() {
|
||||
if (built) {
|
||||
return;
|
||||
}
|
||||
var iterator = replaced.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<Location, BlockData> entry = iterator.next();
|
||||
Location location = entry.getKey();
|
||||
World world = location.getWorld();
|
||||
if (world != null) {
|
||||
if (world != null && world.isChunkLoaded(location.getBlockX() >> 4, location.getBlockZ() >> 4)) {
|
||||
world.getBlockAt(location).setBlockData(entry.getValue(), false);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
replaced.clear();
|
||||
built = false;
|
||||
}
|
||||
|
||||
boolean occupies(org.bukkit.Chunk chunk) {
|
||||
return replaced.keySet().stream().anyMatch(location -> location.getWorld() == chunk.getWorld()
|
||||
&& location.getBlockX() >> 4 == chunk.getX() && location.getBlockZ() >> 4 == chunk.getZ());
|
||||
}
|
||||
|
||||
boolean active() {
|
||||
|
||||
@@ -33,6 +33,9 @@ import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.event.world.ChunkUnloadEvent;
|
||||
import org.bukkit.event.world.EntitiesLoadEvent;
|
||||
import org.bukkit.event.world.EntitiesUnloadEvent;
|
||||
|
||||
public final class RoleArenaController implements Listener, Runnable {
|
||||
private static final String BOSS_TAG = "spigottyrant-role-arena-boss";
|
||||
@@ -40,6 +43,7 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
private final Server server;
|
||||
private final ArenaLocationStore locations;
|
||||
private final PluginSettings settings;
|
||||
private final java.util.function.BiConsumer<Zombie, PluginSettings> configureBossAttributes;
|
||||
private final VigilanteArenaSuccessionService vigilanteSuccession =
|
||||
new VigilanteArenaSuccessionService();
|
||||
private final TyrantArenaSuccessionService tyrantSuccession =
|
||||
@@ -47,27 +51,49 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
private final RoleArenaBarrier barrier = new RoleArenaBarrier();
|
||||
private final Set<UUID> relocating = new HashSet<>();
|
||||
private UUID bossId;
|
||||
private Zombie bossEntity;
|
||||
private UUID challengerId;
|
||||
private boolean spawningBoss;
|
||||
private Zombie spawningBoss;
|
||||
private int spawnRetrySweeps;
|
||||
private boolean spawnWarning;
|
||||
private ArenaRole previousOpenRole;
|
||||
private ArenaRole bossRole;
|
||||
private ArenaRole warnedMissingRole;
|
||||
private ArenaLocation observedLocation;
|
||||
private boolean reconciledLoadedBosses;
|
||||
|
||||
public RoleArenaController(
|
||||
TyrantStateManager stateManager,
|
||||
Server server,
|
||||
ArenaLocationStore locations,
|
||||
PluginSettings settings
|
||||
) {
|
||||
this(stateManager, server, locations, settings, RoleArenaController::configureBossAttributes);
|
||||
}
|
||||
|
||||
// Isolate registry-backed attributes from the lifecycle adapter's headless tests.
|
||||
RoleArenaController(
|
||||
TyrantStateManager stateManager, Server server, ArenaLocationStore locations,
|
||||
PluginSettings settings,
|
||||
java.util.function.BiConsumer<Zombie, PluginSettings> configureBossAttributes
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.server = server;
|
||||
this.locations = locations;
|
||||
this.settings = settings;
|
||||
this.configureBossAttributes = configureBossAttributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (spawnRetrySweeps > 0) {
|
||||
spawnRetrySweeps--;
|
||||
}
|
||||
barrier.restorePending();
|
||||
if (!reconciledLoadedBosses) {
|
||||
server.getWorlds().forEach(world -> removeStaleBosses(world.getEntities()));
|
||||
reconciledLoadedBosses = true;
|
||||
}
|
||||
Optional<ArenaRole> openRole = RoleArenaPolicy.openRole(stateManager.game());
|
||||
Optional<ArenaLocation> configured = locations.location();
|
||||
if (configured.isEmpty()) {
|
||||
@@ -100,8 +126,15 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
}
|
||||
previousOpenRole = role;
|
||||
drawBoundary(world, arena, role);
|
||||
int chunkX = ((int) Math.floor(arena.x())) >> 4;
|
||||
int chunkZ = ((int) Math.floor(arena.z())) >> 4;
|
||||
if (!world.isChunkLoaded(chunkX, chunkZ)
|
||||
|| !world.getChunkAt(chunkX, chunkZ).isEntitiesLoaded()) {
|
||||
closeFight();
|
||||
return;
|
||||
}
|
||||
removeOtherMobs(world, arena);
|
||||
if (role == null) {
|
||||
if (role == null || !hasNearbyPlayer(world, arena)) {
|
||||
closeFight();
|
||||
return;
|
||||
}
|
||||
@@ -109,14 +142,21 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
Zombie boss = boss(world);
|
||||
if (boss == null || bossRole != role) {
|
||||
closeFight();
|
||||
if (spawnRetrySweeps > 0) {
|
||||
return;
|
||||
}
|
||||
boss = spawnBoss(world, arena, role);
|
||||
}
|
||||
if (boss == null) {
|
||||
return;
|
||||
}
|
||||
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()
|
||||
if (challenger == null || !challenger.isOnline() || challenger.isDead()
|
||||
|| !withinBossRange(arena, challenger.getLocation())
|
||||
|| !contains(arena, challenger.getLocation())
|
||||
|| !RoleArenaPolicy.isEligible(
|
||||
stateManager.game(), role, playerState(challenger)
|
||||
@@ -189,10 +229,45 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onChunkUnload(ChunkUnloadEvent event) {
|
||||
Zombie current = bossEntity;
|
||||
boolean bossChunk = current != null && current.getWorld() == event.getWorld()
|
||||
&& (current.getLocation().getBlockX() >> 4) == event.getChunk().getX()
|
||||
&& (current.getLocation().getBlockZ() >> 4) == event.getChunk().getZ();
|
||||
if (bossChunk || barrier.occupies(event.getChunk())) {
|
||||
// Restore walls while block chunks are still loaded, before entities unload.
|
||||
closeFight();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onEntitiesLoad(EntitiesLoadEvent event) {
|
||||
// Event payloads may not yet be visible through world/server entity lookups.
|
||||
removeStaleBosses(event.getEntities());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onEntitiesUnload(EntitiesUnloadEvent event) {
|
||||
removeStaleBosses(event.getEntities());
|
||||
if (event.getEntities().stream().anyMatch(entity -> entity.getUniqueId().equals(bossId))) {
|
||||
closeFight();
|
||||
}
|
||||
}
|
||||
|
||||
private void removeStaleBosses(Iterable<Entity> entities) {
|
||||
for (Entity entity : entities) {
|
||||
if (entity.getScoreboardTags().contains(BOSS_TAG) && !entity.getUniqueId().equals(bossId)) {
|
||||
entity.setPersistent(false);
|
||||
entity.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onCreatureSpawn(CreatureSpawnEvent event) {
|
||||
Optional<ArenaLocation> configured = locations.location();
|
||||
if (!spawningBoss && configured.isPresent()
|
||||
if (event.getEntity() != spawningBoss && configured.isPresent()
|
||||
&& contains(configured.orElseThrow(), event.getLocation())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
@@ -253,8 +328,10 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
return;
|
||||
}
|
||||
UUID defeatedBoss = bossId;
|
||||
Zombie defeatedEntity = bossEntity;
|
||||
ArenaRole defeatedRole = bossRole;
|
||||
bossId = null;
|
||||
bossEntity = null;
|
||||
bossRole = null;
|
||||
Player killer = event.getEntity().getKiller();
|
||||
if (killer != null && challengerId != null && defeatedRole != null
|
||||
@@ -266,6 +343,7 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
return;
|
||||
}
|
||||
bossId = defeatedBoss;
|
||||
bossEntity = defeatedEntity;
|
||||
bossRole = defeatedRole;
|
||||
resetFight();
|
||||
}
|
||||
@@ -276,13 +354,14 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
|
||||
private boolean enforcePosition(Player player, Location from, Location to) {
|
||||
Optional<ArenaLocation> configured = locations.location();
|
||||
if (configured.isEmpty() || relocating.contains(player.getUniqueId())) {
|
||||
if (configured.isEmpty() || !player.isOnline() || player.isDead()
|
||||
|| relocating.contains(player.getUniqueId())) {
|
||||
return false;
|
||||
}
|
||||
ArenaLocation arena = configured.orElseThrow();
|
||||
boolean inside = contains(arena, to);
|
||||
if (player.getUniqueId().equals(challengerId)) {
|
||||
if (!inside) {
|
||||
if (!inside || !withinBossRange(arena, to)) {
|
||||
resetFight();
|
||||
}
|
||||
return false;
|
||||
@@ -291,23 +370,29 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
return false;
|
||||
}
|
||||
PlayerState state = playerState(player);
|
||||
if (bossId == null && withinBossRange(arena, to)) {
|
||||
Optional<ArenaRole> openRole = RoleArenaPolicy.openRole(stateManager.game());
|
||||
if (openRole.isPresent() && RoleArenaPolicy.isEligible(
|
||||
stateManager.game(), openRole.orElseThrow(), state)) {
|
||||
// Allow an eligible arrival to commit; the sweep summons/admits, not a
|
||||
// proposed teleport which another listener could still cancel.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ArenaRole role = bossRole;
|
||||
if (challengerId == null && bossId != null && role != null
|
||||
if (challengerId == null && bossId != null && role != null && withinBossRange(arena, to)
|
||||
&& RoleArenaPolicy.isEligible(stateManager.game(), role, state)) {
|
||||
challengerId = player.getUniqueId();
|
||||
World world = server.getWorld(arena.worldName());
|
||||
if (world != null) {
|
||||
barrier.build(
|
||||
Zombie boss = world == null ? null : boss(world);
|
||||
if (boss == null || !barrier.build(
|
||||
world, arena, settings.vigilanteArenaRadiusBlocks(),
|
||||
settings.arenaBarrierHeightBlocks()
|
||||
);
|
||||
Zombie boss = boss(world);
|
||||
if (boss != null) {
|
||||
settings.arenaBarrierHeightBlocks())) {
|
||||
return false;
|
||||
}
|
||||
challengerId = player.getUniqueId();
|
||||
boss.setHealth(settings.vigilanteBossHealth());
|
||||
boss.teleport(toBukkit(world, arena));
|
||||
ArenaBossState.engage(boss, player);
|
||||
}
|
||||
}
|
||||
player.sendMessage(ChatColor.GOLD + "Defeat the arena boss to become "
|
||||
+ readable(role) + "!");
|
||||
return false;
|
||||
@@ -350,25 +435,22 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
private void closeFight() {
|
||||
challengerId = null;
|
||||
barrier.clear();
|
||||
if (bossId != null) {
|
||||
Entity entity = server.getEntity(bossId);
|
||||
if (entity != null) {
|
||||
entity.remove();
|
||||
}
|
||||
Zombie retiring = bossEntity;
|
||||
bossEntity = null;
|
||||
bossId = null;
|
||||
}
|
||||
bossRole = null;
|
||||
if (retiring != null) {
|
||||
bossParticles(retiring.getLocation());
|
||||
retiring.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private Zombie spawnBoss(World world, ArenaLocation arena, ArenaRole role) {
|
||||
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 -> {
|
||||
spawningBoss = zombie;
|
||||
zombie.setPersistent(false);
|
||||
zombie.addScoreboardTag(BOSS_TAG);
|
||||
zombie.setAdult();
|
||||
zombie.setCustomName(role == ArenaRole.TYRANT
|
||||
? ChatColor.RED + "Tyrant's Trial"
|
||||
@@ -377,27 +459,48 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
zombie.setRemoveWhenFarAway(false);
|
||||
zombie.setCanPickupItems(false);
|
||||
zombie.setAI(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());
|
||||
configureBossAttributes.accept(zombie, settings);
|
||||
zombie.setHealth(settings.vigilanteBossHealth());
|
||||
});
|
||||
spawningBoss = boss;
|
||||
// Reassert after spawn listeners and never own a cancelled/removed candidate.
|
||||
boss.setPersistent(false);
|
||||
if (!boss.isValid() || boss.isDead()) {
|
||||
boss.remove();
|
||||
spawnFailed();
|
||||
return null;
|
||||
}
|
||||
spawnWarning = false;
|
||||
bossEntity = boss;
|
||||
bossId = boss.getUniqueId();
|
||||
bossRole = role;
|
||||
bossParticles(boss.getLocation());
|
||||
return boss;
|
||||
} catch (RuntimeException exception) {
|
||||
bossEntity = null;
|
||||
bossId = null;
|
||||
bossRole = null;
|
||||
if (spawningBoss != null) {
|
||||
spawningBoss.remove();
|
||||
}
|
||||
spawnFailed();
|
||||
return null;
|
||||
} finally {
|
||||
spawningBoss = false;
|
||||
spawningBoss = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnFailed() {
|
||||
spawnRetrySweeps = 10; // Five seconds at the controller's ten-tick cadence.
|
||||
if (!spawnWarning) {
|
||||
server.getLogger().warning("Tyrant arena boss spawn failed or was cancelled; retrying in five seconds.");
|
||||
spawnWarning = true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return bossEntity != null && bossEntity.getWorld().equals(world)
|
||||
&& bossEntity.isValid() && !bossEntity.isDead() ? bossEntity : null;
|
||||
}
|
||||
|
||||
void assignRole(UUID playerId, ArenaRole role) {
|
||||
@@ -528,6 +631,26 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private void bossParticles(Location location) {
|
||||
World world = location.getWorld();
|
||||
if (world != null && world.isChunkLoaded(location.getBlockX() >> 4, location.getBlockZ() >> 4)) {
|
||||
world.spawnParticle(Particle.PORTAL, location.clone().add(0, 1, 0),
|
||||
60, 0.6, 1.0, 0.6, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasNearbyPlayer(World world, ArenaLocation arena) {
|
||||
return world.getPlayers().stream().anyMatch(player -> player.isOnline() && !player.isDead()
|
||||
&& withinBossRange(arena, player.getLocation()));
|
||||
}
|
||||
|
||||
private boolean withinBossRange(ArenaLocation arena, Location location) {
|
||||
World world = location.getWorld();
|
||||
double range = settings.vigilanteArenaRadiusBlocks() + 8.0;
|
||||
return world != null && world.getName().equals(arena.worldName())
|
||||
&& location.distanceSquared(toBukkit(world, arena)) <= range * range;
|
||||
}
|
||||
|
||||
private boolean protects(Location location) {
|
||||
Optional<ArenaLocation> configured = locations.location();
|
||||
World world = location.getWorld();
|
||||
@@ -603,6 +726,12 @@ public final class RoleArenaController implements Listener, Runnable {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void configureBossAttributes(Zombie zombie, PluginSettings settings) {
|
||||
setAttribute(zombie, Attribute.MAX_HEALTH, settings.vigilanteBossHealth());
|
||||
setAttribute(zombie, Attribute.ATTACK_DAMAGE, settings.vigilanteBossDamage());
|
||||
setAttribute(zombie, Attribute.ARMOR, settings.vigilanteBossArmor());
|
||||
}
|
||||
|
||||
private static void setAttribute(Zombie zombie, Attribute attribute, double value) {
|
||||
if (zombie.getAttribute(attribute) != null) {
|
||||
zombie.getAttribute(attribute).setBaseValue(value);
|
||||
|
||||
@@ -23,9 +23,45 @@ final class RoleArenaBarrierTest {
|
||||
"world", 0.0, 64.0, 0.0, 0.0F, 0.0F
|
||||
);
|
||||
|
||||
@Test
|
||||
void missingPerimeterChunkDoesNotLoadOrPartiallyBuild() {
|
||||
World world = mock(World.class);
|
||||
Block solid = mock(Block.class);
|
||||
when(solid.getType()).thenReturn(Material.STONE);
|
||||
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenReturn(solid);
|
||||
RoleArenaBarrier barrier = new RoleArenaBarrier();
|
||||
barrier.build(world, CENTER, 10.0, 12);
|
||||
verify(world, never()).getBlockAt(anyInt(), anyInt(), anyInt());
|
||||
org.junit.jupiter.api.Assertions.assertFalse(barrier.active());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restorationWaitsForLoadedChunksWithoutLoadingThem() {
|
||||
World world = mock(World.class);
|
||||
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
Block air = mock(Block.class);
|
||||
BlockData original = mock(BlockData.class);
|
||||
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenReturn(air);
|
||||
when(world.getBlockAt(any(Location.class))).thenReturn(air);
|
||||
when(air.getLocation()).thenReturn(new Location(world, 1, 64, 0));
|
||||
when(air.getType()).thenReturn(Material.AIR);
|
||||
when(air.getBlockData()).thenReturn(original);
|
||||
when(original.clone()).thenReturn(original);
|
||||
RoleArenaBarrier barrier = new RoleArenaBarrier();
|
||||
barrier.build(world, CENTER, 10, 12);
|
||||
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(false);
|
||||
barrier.clear();
|
||||
verify(world, never()).getBlockAt(any(Location.class));
|
||||
verify(air, never()).setBlockData(any(), org.mockito.ArgumentMatchers.eq(false));
|
||||
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
barrier.clear();
|
||||
verify(air).setBlockData(original, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotReplaceExistingNonAirBlocks() {
|
||||
World world = mock(World.class);
|
||||
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
Block solid = mock(Block.class);
|
||||
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenReturn(solid);
|
||||
when(solid.getLocation()).thenReturn(new Location(world, 1.0, 64.0, 0.0));
|
||||
@@ -39,6 +75,7 @@ final class RoleArenaBarrierTest {
|
||||
@Test
|
||||
void restoresAirReplacedByTemporaryBarrier() {
|
||||
World world = mock(World.class);
|
||||
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
Block air = mock(Block.class);
|
||||
BlockData original = mock(BlockData.class);
|
||||
BlockData snapshot = mock(BlockData.class);
|
||||
@@ -60,6 +97,7 @@ final class RoleArenaBarrierTest {
|
||||
@Test
|
||||
void wallExtendsOneBelowFloorForTwelveBlocks() {
|
||||
World world = mock(World.class);
|
||||
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
Block solid = mock(Block.class);
|
||||
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenReturn(solid);
|
||||
when(solid.getLocation()).thenReturn(new Location(world, 1.0, 64.0, 0.0));
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Zombie;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class RoleArenaLifecycleTest {
|
||||
@Test
|
||||
void vacantArenaWithoutNearbyPlayersDoesNotSpawnABoss() {
|
||||
Fixture f = new Fixture();
|
||||
f.controller.run();
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.EnumSource(ArenaRole.class)
|
||||
void sameInclusiveThresholdSpawnsOnceAndDespawnsWithParticles(ArenaRole role) {
|
||||
Fixture f = new Fixture();
|
||||
if (role == ArenaRole.VIGILANTE) {
|
||||
when(f.manager.game()).thenReturn(Fixture.vigilanteVacancy());
|
||||
}
|
||||
Player player = f.player(18.01, 64, 0);
|
||||
f.controller.run();
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any());
|
||||
when(player.getLocation()).thenReturn(new Location(f.world, 18, 64, 0));
|
||||
f.controller.run();
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.spawned.size());
|
||||
verify(f.world).spawnParticle(eq(org.bukkit.Particle.PORTAL), any(Location.class),
|
||||
eq(60), eq(0.6), eq(1.0), eq(0.6), eq(0.05));
|
||||
when(player.getLocation()).thenReturn(new Location(f.world, 18.01, 64, 0));
|
||||
f.controller.run();
|
||||
f.controller.run();
|
||||
verify(f.spawned.get(0)).remove();
|
||||
verify(f.world, times(2)).spawnParticle(eq(org.bukkit.Particle.PORTAL), any(Location.class),
|
||||
eq(60), eq(0.6), eq(1.0), eq(0.6), eq(0.05));
|
||||
}
|
||||
|
||||
@Test
|
||||
void waitsForChunkEntitiesWithoutLoadingAnAbsentChunk() {
|
||||
Fixture f = new Fixture();
|
||||
f.player(15, 64, 0);
|
||||
when(f.world.isChunkLoaded(0, 0)).thenReturn(false);
|
||||
f.controller.run();
|
||||
verify(f.world, never()).getChunkAt(0, 0);
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any());
|
||||
when(f.world.isChunkLoaded(0, 0)).thenReturn(true);
|
||||
when(f.chunk.isEntitiesLoaded()).thenReturn(false);
|
||||
f.controller.run();
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any());
|
||||
when(f.chunk.isEntitiesLoaded()).thenReturn(true);
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.spawned.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transientServerLookupMissDoesNotDuplicateOrOrphanOwnedBoss() {
|
||||
Fixture f = new Fixture();
|
||||
f.player(15, 64, 0);
|
||||
f.controller.run();
|
||||
when(f.server.getEntity(any(UUID.class))).thenReturn(null);
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.spawned.size());
|
||||
f.players.clear();
|
||||
f.controller.run();
|
||||
verify(f.spawned.get(0)).remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
void startupAndLateEntityLoadsRemoveOnlyLegacyTaggedBosses() {
|
||||
Fixture f = new Fixture();
|
||||
Zombie legacy = f.taggedBoss();
|
||||
Zombie ordinary = mock(Zombie.class);
|
||||
when(ordinary.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
f.entities.put(ordinary.getUniqueId(), ordinary);
|
||||
f.controller.run();
|
||||
verify(legacy).remove();
|
||||
verify(ordinary, never()).remove();
|
||||
f.player(15, 64, 0);
|
||||
f.controller.run();
|
||||
Zombie current = f.spawned.get(0);
|
||||
Zombie lateLegacy = f.taggedBoss();
|
||||
f.fire(new org.bukkit.event.world.EntitiesLoadEvent(f.chunk, List.of(lateLegacy, current, ordinary)));
|
||||
verify(lateLegacy).remove();
|
||||
verify(current, never()).remove();
|
||||
verify(ordinary, never()).remove();
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.spawned.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void entityUnloadRetiresOwnedBossAndLateOldEventsDoNotRetireReplacement() {
|
||||
Fixture f = new Fixture();
|
||||
f.player(15, 64, 0);
|
||||
f.controller.run();
|
||||
Zombie old = f.spawned.get(0);
|
||||
f.fire(new org.bukkit.event.world.EntitiesUnloadEvent(f.chunk, List.of(old)));
|
||||
verify(old).remove();
|
||||
when(f.chunk.isEntitiesLoaded()).thenReturn(false);
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.spawned.size());
|
||||
when(f.chunk.isEntitiesLoaded()).thenReturn(true);
|
||||
f.controller.run();
|
||||
Zombie replacement = f.spawned.get(1);
|
||||
f.fire(new org.bukkit.event.world.EntitiesUnloadEvent(f.chunk, List.of(old)));
|
||||
f.controller.run();
|
||||
verify(replacement, never()).remove();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(2, f.spawned.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulBossIsNonPersistentAndCancelledSpawnHasNoArrivalBurst() {
|
||||
Fixture f = new Fixture();
|
||||
f.player(15, 64, 0);
|
||||
Zombie rejected = f.taggedBoss();
|
||||
f.entities.remove(rejected.getUniqueId());
|
||||
when(f.world.spawn(any(Location.class), eq(Zombie.class), any())).thenReturn(rejected);
|
||||
f.controller.run();
|
||||
verify(rejected).remove();
|
||||
verify(f.world, never()).spawnParticle(eq(org.bukkit.Particle.PORTAL), any(Location.class),
|
||||
eq(60), anyDouble(), anyDouble(), anyDouble(), anyDouble());
|
||||
f.controller.close();
|
||||
verify(rejected).remove();
|
||||
Fixture good = new Fixture();
|
||||
good.player(15, 64, 0);
|
||||
good.controller.run();
|
||||
verify(good.spawned.get(0), times(2)).setPersistent(false);
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"walk", "fly", "teleport", "world", "logout", "death"})
|
||||
void lastPlayerDepartureByAnyMechanismDespawnsOnce(String mechanism) {
|
||||
Fixture f = new Fixture();
|
||||
Player player = f.player(15, 64, 0);
|
||||
f.controller.run();
|
||||
f.depart(player, mechanism);
|
||||
f.controller.run();
|
||||
f.controller.run();
|
||||
verify(f.spawned.get(0)).remove();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.spawned.size());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"walk", "fly", "teleport", "world", "logout", "death"})
|
||||
void departingChallengerResetsFightButAnotherNearbyPlayerKeepsBoss(String mechanism) {
|
||||
Fixture f = new Fixture();
|
||||
Player challenger = f.player(0, 64, 0);
|
||||
f.player(15, 64, 0);
|
||||
f.controller.run();
|
||||
Zombie boss = f.spawned.get(0);
|
||||
verify(boss).setTarget(challenger);
|
||||
clearInvocations(boss);
|
||||
f.depart(challenger, mechanism);
|
||||
f.controller.run();
|
||||
verify(boss, never()).remove();
|
||||
verify(boss).setAI(false);
|
||||
verify(boss).setTarget(null);
|
||||
verify(boss, never()).setTarget(challenger);
|
||||
org.bukkit.event.block.BlockBreakEvent blockBreak = mock(org.bukkit.event.block.BlockBreakEvent.class);
|
||||
org.bukkit.block.Block block = mock(org.bukkit.block.Block.class);
|
||||
when(block.getLocation()).thenReturn(new Location(f.world, 0, 64, 0));
|
||||
when(blockBreak.getBlock()).thenReturn(block);
|
||||
f.controller.onBlockBreak(blockBreak);
|
||||
verify(blockBreak, never()).setCancelled(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingBarrierChunkCannotStartFightAndUnloadEndsActiveFight() {
|
||||
Fixture f = new Fixture();
|
||||
Player player = f.player(0, 64, 0);
|
||||
when(f.world.isChunkLoaded(-1, -1)).thenReturn(false);
|
||||
f.controller.run();
|
||||
Zombie boss = f.spawned.get(0);
|
||||
verify(boss, never()).setTarget(player);
|
||||
when(f.world.isChunkLoaded(-1, -1)).thenReturn(true);
|
||||
f.controller.run();
|
||||
verify(boss).setTarget(player);
|
||||
f.fire(new org.bukkit.event.world.ChunkUnloadEvent(f.chunk));
|
||||
verify(boss).remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
void roleChangesPauseAndRestartNeverLeaveTwoBosses() {
|
||||
Fixture f = new Fixture();
|
||||
f.player(15, 64, 0);
|
||||
f.controller.run();
|
||||
Zombie tyrant = f.spawned.get(0);
|
||||
when(f.manager.game()).thenReturn(Fixture.vigilanteVacancy());
|
||||
f.controller.run();
|
||||
f.controller.run();
|
||||
verify(tyrant).remove();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.entities.size());
|
||||
GameState paused = new GameState(GameLifecycle.PAUSED, Optional.of(UUID.randomUUID()),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(java.time.Instant.EPOCH),
|
||||
Duration.ZERO, 0, 0, Set.of());
|
||||
when(f.manager.game()).thenReturn(paused);
|
||||
f.controller.run();
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(0, f.entities.size());
|
||||
when(f.manager.game()).thenReturn(Fixture.vacancy());
|
||||
f.controller.run();
|
||||
Zombie beforeRestart = f.spawned.get(2);
|
||||
ArenaLocationStore locations = mock(ArenaLocationStore.class);
|
||||
when(locations.location()).thenReturn(Optional.of(f.center));
|
||||
RoleArenaController restarted = new RoleArenaController(f.manager, f.server, locations,
|
||||
PluginSettings.from(Map.of()), (boss, settings) -> { });
|
||||
f.eventController = restarted;
|
||||
restarted.run();
|
||||
restarted.run();
|
||||
verify(beforeRestart).remove();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.entities.size());
|
||||
restarted.close();
|
||||
restarted.close();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(0, f.entities.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedSpawnLeavesNoOwnershipAndBacksOffBeforeRetry() {
|
||||
Fixture f = new Fixture();
|
||||
f.player(15, 64, 0);
|
||||
when(f.world.spawn(any(Location.class), eq(Zombie.class), any()))
|
||||
.thenThrow(new IllegalStateException("spawn rejected"));
|
||||
org.junit.jupiter.api.Assertions.assertDoesNotThrow(f.controller::run);
|
||||
for (int i = 0; i < 9; i++) {
|
||||
f.controller.run();
|
||||
}
|
||||
verify(f.world).spawn(any(Location.class), eq(Zombie.class), any());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(0, f.entities.size());
|
||||
f.controller.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeFightStillExcludesNonChallengersAboveTheRing() {
|
||||
Fixture f = new Fixture();
|
||||
f.player(0, 64, 0);
|
||||
f.controller.run();
|
||||
Player intruder = f.player(0, 83, 0);
|
||||
f.controller.run();
|
||||
verify(intruder).teleport(any(Location.class),
|
||||
eq(org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.PLUGIN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void initializerTagsBeforeAdmissionAndExemptsOnlyItsOwnCandidate() {
|
||||
Fixture f = new Fixture();
|
||||
f.player(15, 64, 0);
|
||||
f.duringSpawn = boss -> {
|
||||
verify(boss).setPersistent(false);
|
||||
org.junit.jupiter.api.Assertions.assertTrue(boss.getScoreboardTags()
|
||||
.contains("spigottyrant-role-arena-boss"));
|
||||
Zombie unrelated = f.newZombie();
|
||||
var nested = new org.bukkit.event.entity.CreatureSpawnEvent(unrelated,
|
||||
org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CUSTOM);
|
||||
f.fire(nested);
|
||||
org.junit.jupiter.api.Assertions.assertTrue(nested.isCancelled());
|
||||
};
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.entities.size());
|
||||
verify(f.spawned.get(0), never()).remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionAfterCandidateInitializationRemovesThatCandidate() {
|
||||
Fixture f = new Fixture();
|
||||
f.player(15, 64, 0);
|
||||
f.duringSpawn = boss -> { throw new IllegalStateException("spawn listener failed"); };
|
||||
org.junit.jupiter.api.Assertions.assertDoesNotThrow(f.controller::run);
|
||||
verify(f.spawned.get(0)).remove();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(0, f.entities.size());
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.spawned.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unloadingAWallOnlyChunkRestoresWallsAndEndsFight() {
|
||||
Fixture f = new Fixture();
|
||||
org.bukkit.block.Block left = mock(org.bukkit.block.Block.class);
|
||||
org.bukkit.block.Block right = mock(org.bukkit.block.Block.class);
|
||||
org.bukkit.block.data.BlockData original = mock(org.bukkit.block.data.BlockData.class);
|
||||
when(original.clone()).thenReturn(original);
|
||||
for (org.bukkit.block.Block block : List.of(left, right)) {
|
||||
when(block.getType()).thenReturn(org.bukkit.Material.AIR);
|
||||
when(block.getBlockData()).thenReturn(original);
|
||||
}
|
||||
when(left.getLocation()).thenReturn(new Location(f.world, -1, 64, 0));
|
||||
when(right.getLocation()).thenReturn(new Location(f.world, 1, 64, 0));
|
||||
when(f.world.getBlockAt(anyInt(), anyInt(), anyInt()))
|
||||
.thenAnswer(call -> (int) call.getArgument(0) < 0 ? left : right);
|
||||
when(f.world.getBlockAt(any(Location.class)))
|
||||
.thenAnswer(call -> ((Location) call.getArgument(0)).getX() < 0 ? left : right);
|
||||
f.player(0, 64, 0);
|
||||
f.controller.run();
|
||||
verify(left).setType(org.bukkit.Material.BARRIER, false);
|
||||
verify(right).setType(org.bukkit.Material.BARRIER, false);
|
||||
Chunk wallChunk = mock(Chunk.class);
|
||||
when(wallChunk.getWorld()).thenReturn(f.world);
|
||||
when(wallChunk.getX()).thenReturn(-1);
|
||||
when(wallChunk.getZ()).thenReturn(0);
|
||||
f.fire(new org.bukkit.event.world.ChunkUnloadEvent(wallChunk));
|
||||
verify(left).setBlockData(original, false);
|
||||
verify(right).setBlockData(original, false);
|
||||
verify(f.spawned.get(0)).remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
void eligibleTeleportArrivalWaitsForCommittedPositionBeforeSummoning() {
|
||||
Fixture f = new Fixture();
|
||||
Player player = f.player(100, 64, 0);
|
||||
f.controller.run();
|
||||
var arrival = new org.bukkit.event.player.PlayerTeleportEvent(player,
|
||||
player.getLocation(), new Location(f.world, 0, 64, 0));
|
||||
f.controller.onTeleport(arrival);
|
||||
org.junit.jupiter.api.Assertions.assertFalse(arrival.isCancelled());
|
||||
// Another plugin may still cancel: no boss until the player's location actually changes.
|
||||
arrival.setCancelled(true);
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(0, f.spawned.size());
|
||||
when(player.getLocation()).thenReturn(new Location(f.world, 0, 64, 0));
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.spawned.size());
|
||||
verify(f.spawned.get(0)).setTarget(player);
|
||||
}
|
||||
|
||||
static final class Fixture {
|
||||
final World world = mock(World.class);
|
||||
final World otherWorld = mock(World.class);
|
||||
final Server server = mock(Server.class);
|
||||
final Chunk chunk = mock(Chunk.class);
|
||||
final TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
final ArenaLocation center = new ArenaLocation("world", 0, 64, 0, 0, 0);
|
||||
final List<Player> players = new ArrayList<>();
|
||||
final List<Zombie> spawned = new ArrayList<>();
|
||||
final Map<UUID, Entity> entities = new HashMap<>();
|
||||
final RoleArenaController controller;
|
||||
RoleArenaController eventController;
|
||||
java.util.function.Consumer<Zombie> duringSpawn = boss -> { };
|
||||
|
||||
Fixture() {
|
||||
when(world.getName()).thenReturn("world");
|
||||
when(otherWorld.getName()).thenReturn("other");
|
||||
when(world.getMinHeight()).thenReturn(-64);
|
||||
when(world.getMaxHeight()).thenReturn(320);
|
||||
org.bukkit.block.Block ground = mock(org.bukkit.block.Block.class);
|
||||
when(ground.getType()).thenReturn(org.bukkit.Material.STONE);
|
||||
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenReturn(ground);
|
||||
when(world.getPlayers()).thenAnswer(call -> List.copyOf(players));
|
||||
when(world.getEntities()).thenAnswer(call -> List.copyOf(entities.values()));
|
||||
when(world.getNearbyEntities(any(Location.class), anyDouble(), anyDouble(), anyDouble()))
|
||||
.thenReturn(List.of());
|
||||
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
when(world.getChunkAt(0, 0)).thenReturn(chunk);
|
||||
when(chunk.isEntitiesLoaded()).thenReturn(true);
|
||||
when(chunk.getWorld()).thenReturn(world);
|
||||
when(chunk.getEntities()).thenAnswer(call -> entities.values().toArray(Entity[]::new));
|
||||
when(server.getWorld("world")).thenReturn(world);
|
||||
when(server.getLogger()).thenReturn(mock(java.util.logging.Logger.class));
|
||||
when(server.getWorlds()).thenReturn(List.of(world));
|
||||
when(server.getEntity(any(UUID.class))).thenAnswer(call -> entities.get(call.getArgument(0)));
|
||||
when(server.getOnlinePlayers()).thenAnswer(call -> List.copyOf(players));
|
||||
when(server.getPlayer(any(UUID.class))).thenAnswer(call -> players.stream()
|
||||
.filter(player -> player.getUniqueId().equals(call.getArgument(0)))
|
||||
.findFirst().orElse(null));
|
||||
when(manager.game()).thenReturn(vacancy());
|
||||
when(manager.players()).thenAnswer(call -> {
|
||||
Map<UUID, PlayerState> states = new HashMap<>();
|
||||
players.forEach(player -> states.put(player.getUniqueId(),
|
||||
PlayerState.newPlayer(player.getUniqueId(), "Visitor")));
|
||||
return states;
|
||||
});
|
||||
when(world.spawn(any(Location.class), eq(Zombie.class), any())).thenAnswer(call -> {
|
||||
Zombie boss = newZombie();
|
||||
spawned.add(boss);
|
||||
java.util.function.Consumer<Zombie> initializer = call.getArgument(2);
|
||||
initializer.accept(boss);
|
||||
duringSpawn.accept(boss);
|
||||
var event = new org.bukkit.event.entity.CreatureSpawnEvent(boss,
|
||||
org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CUSTOM);
|
||||
fire(event);
|
||||
if (!event.isCancelled()) {
|
||||
entities.put(boss.getUniqueId(), boss);
|
||||
}
|
||||
return boss;
|
||||
});
|
||||
ArenaLocationStore locations = mock(ArenaLocationStore.class);
|
||||
when(locations.location()).thenReturn(Optional.of(center));
|
||||
controller = new RoleArenaController(manager, server, locations, PluginSettings.from(Map.of()),
|
||||
(boss, settings) -> { });
|
||||
eventController = controller;
|
||||
}
|
||||
|
||||
void depart(Player player, String mechanism) {
|
||||
switch (mechanism) {
|
||||
case "walk" -> when(player.getLocation()).thenReturn(new Location(world, 18.01, 64, 0));
|
||||
case "fly" -> when(player.getLocation()).thenReturn(new Location(world, 0, 82.01, 0));
|
||||
case "teleport" -> when(player.getLocation()).thenReturn(new Location(world, 1000, 64, 0));
|
||||
case "world" -> when(player.getLocation()).thenReturn(new Location(otherWorld, 0, 64, 0));
|
||||
case "logout" -> when(player.isOnline()).thenReturn(false);
|
||||
case "death" -> when(player.isDead()).thenReturn(true);
|
||||
default -> throw new IllegalArgumentException(mechanism);
|
||||
}
|
||||
}
|
||||
|
||||
void fire(org.bukkit.event.Event event) {
|
||||
for (var method : RoleArenaController.class.getMethods()) {
|
||||
if (method.isAnnotationPresent(org.bukkit.event.EventHandler.class)
|
||||
&& method.getParameterCount() == 1
|
||||
&& method.getParameterTypes()[0].isInstance(event)) {
|
||||
try {
|
||||
method.invoke(eventController, event);
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Player player(double x, double y, double z) {
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(player.getName()).thenReturn("Visitor");
|
||||
when(player.isOnline()).thenReturn(true);
|
||||
when(player.getLocation()).thenReturn(new Location(world, x, y, z));
|
||||
players.add(player);
|
||||
return player;
|
||||
}
|
||||
|
||||
Zombie taggedBoss() {
|
||||
Zombie boss = newZombie();
|
||||
boss.addScoreboardTag("spigottyrant-role-arena-boss");
|
||||
entities.put(boss.getUniqueId(), boss);
|
||||
return boss;
|
||||
}
|
||||
|
||||
Zombie newZombie() {
|
||||
Zombie boss = mock(Zombie.class);
|
||||
UUID id = UUID.randomUUID();
|
||||
when(boss.getUniqueId()).thenReturn(id);
|
||||
when(boss.getWorld()).thenReturn(world);
|
||||
when(boss.getLocation()).thenReturn(new Location(world, 0, 64, 0));
|
||||
Set<String> tags = new java.util.HashSet<>();
|
||||
when(boss.getScoreboardTags()).thenReturn(tags);
|
||||
when(boss.addScoreboardTag(anyString())).thenAnswer(call -> tags.add(call.getArgument(0)));
|
||||
when(boss.isValid()).thenAnswer(call -> entities.containsKey(id));
|
||||
doAnswer(call -> { entities.remove(id); return null; }).when(boss).remove();
|
||||
return boss;
|
||||
}
|
||||
|
||||
static GameState vigilanteVacancy() {
|
||||
return new GameState(GameLifecycle.RUNNING, Optional.of(UUID.randomUUID()), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO, 0, 0, Set.of());
|
||||
}
|
||||
|
||||
static GameState vacancy() {
|
||||
return new GameState(GameLifecycle.RUNNING, Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO, 0, 1, Set.of());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user