fix(arena): improve containment and boss idle
Release / release (push) Successful in 2m31s
CI / build (push) Successful in 1m8s

This commit is contained in:
dmg
2026-08-23 18:13:24 -04:00
parent 9ed0eed832
commit f0dbde17b8
16 changed files with 250 additions and 34 deletions
@@ -0,0 +1,22 @@
package games.dmg.spigottyrant;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.entity.Zombie;
final class ArenaBossState {
private ArenaBossState() {
}
static void idle(Zombie boss, Location center, double health) {
boss.setAI(false);
boss.setTarget(null);
boss.setHealth(health);
boss.teleport(center);
}
static void engage(Zombie boss, Player challenger) {
boss.setAI(true);
boss.setTarget(challenger);
}
}
@@ -0,0 +1,20 @@
package games.dmg.spigottyrant;
final class ArenaBuildProtection {
private ArenaBuildProtection() {
}
static boolean isProtected(
boolean fightActive,
ArenaLocation center,
double radius,
String worldName,
double x,
double y,
double z
) {
return fightActive && ArenaGeometry.contains(
center, radius, worldName, x, y, z
);
}
}
@@ -13,6 +13,7 @@ public record PluginSettings(
double tyrantRangeBlocks,
double followerRangeBlocks,
double vigilanteArenaRadiusBlocks,
int arenaBarrierHeightBlocks,
double vigilanteBossHealth,
double vigilanteBossDamage,
double vigilanteBossArmor,
@@ -53,6 +54,11 @@ public record PluginSettings(
requireFiniteRange(
vigilanteArenaRadiusBlocks, 1.0, 512.0, "vigilante-arena-radius-blocks"
);
if (arenaBarrierHeightBlocks < 3 || arenaBarrierHeightBlocks > 128) {
throw new IllegalArgumentException(
"arena-barrier-height-blocks must be between 3 and 128"
);
}
requireFiniteRange(vigilanteBossHealth, 1.0, 1024.0, "vigilante-boss-health");
requireFiniteRange(vigilanteBossDamage, 0.1, 2048.0, "vigilante-boss-damage");
requireFiniteRange(vigilanteBossArmor, 0.0, 30.0, "vigilante-boss-armor");
@@ -100,6 +106,7 @@ public record PluginSettings(
decimal(values, "tyrant-range-blocks", 50.0),
decimal(values, "follower-range-blocks", 50.0),
decimal(values, "vigilante-arena-radius-blocks", 10.0),
integer(values, "arena-barrier-height-blocks", 12),
decimal(values, "vigilante-boss-health", 80.0),
decimal(values, "vigilante-boss-damage", 8.0),
decimal(values, "vigilante-boss-armor", 10.0),
@@ -8,22 +8,27 @@ import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
final class VigilanteArenaBarrier {
final class RoleArenaBarrier {
private final Map<Location, BlockData> replaced = new LinkedHashMap<>();
private boolean built;
void build(World world, ArenaLocation center, double radius) {
void build(
World world,
ArenaLocation center,
double radius,
int wallHeight
) {
if (built) {
return;
}
built = true;
int samples = Math.max(64, (int) Math.ceil(2.0 * Math.PI * radius * 2.0));
int baseY = (int) Math.floor(center.y());
int baseY = (int) Math.floor(center.y()) - 1;
for (int index = 0; index < samples; index++) {
double angle = 2.0 * Math.PI * index / samples;
int x = (int) Math.floor(center.x() + Math.cos(angle) * radius);
int z = (int) Math.floor(center.z() + Math.sin(angle) * radius);
for (int y = baseY; y < baseY + 3; y++) {
for (int y = baseY; y < baseY + wallHeight; y++) {
Block block = world.getBlockAt(x, y, z);
Location location = block.getLocation();
if (replaced.containsKey(location) || !isAir(block.getType())) {
@@ -26,6 +26,8 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
@@ -42,7 +44,7 @@ public final class RoleArenaController implements Listener, Runnable {
new VigilanteArenaSuccessionService();
private final TyrantArenaSuccessionService tyrantSuccession =
new TyrantArenaSuccessionService();
private final VigilanteArenaBarrier barrier = new VigilanteArenaBarrier();
private final RoleArenaBarrier barrier = new RoleArenaBarrier();
private final Set<UUID> relocating = new HashSet<>();
private UUID bossId;
private UUID challengerId;
@@ -121,10 +123,13 @@ public final class RoleArenaController implements Listener, Runnable {
)) {
if (challengerId != null) {
resetFight();
} else {
ArenaBossState.idle(
boss, toBukkit(world, arena), settings.vigilanteBossHealth()
);
}
boss.setTarget(null);
} else {
boss.setTarget(challenger);
ArenaBossState.engage(boss, challenger);
}
for (Player player : world.getPlayers()) {
enforcePosition(player, null, player.getLocation());
@@ -193,6 +198,24 @@ public final class RoleArenaController implements Listener, Runnable {
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockPlace(BlockPlaceEvent event) {
if (protects(event.getBlock().getLocation())) {
event.setCancelled(true);
event.getPlayer().sendMessage(ChatColor.RED
+ "Blocks cannot be changed during an arena fight.");
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockBreak(BlockBreakEvent event) {
if (protects(event.getBlock().getLocation())) {
event.setCancelled(true);
event.getPlayer().sendMessage(ChatColor.RED
+ "Blocks cannot be changed during an arena fight.");
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onCombust(EntityCombustEvent event) {
if (event.getEntity().getUniqueId().equals(bossId)) {
@@ -274,12 +297,15 @@ public final class RoleArenaController implements Listener, Runnable {
challengerId = player.getUniqueId();
World world = server.getWorld(arena.worldName());
if (world != null) {
barrier.build(world, arena, settings.vigilanteArenaRadiusBlocks());
barrier.build(
world, arena, settings.vigilanteArenaRadiusBlocks(),
settings.arenaBarrierHeightBlocks()
);
Zombie boss = boss(world);
if (boss != null) {
boss.setHealth(settings.vigilanteBossHealth());
boss.teleport(toBukkit(world, arena));
boss.setTarget(player);
ArenaBossState.engage(boss, player);
}
}
player.sendMessage(ChatColor.GOLD + "Defeat the arena boss to become "
@@ -312,9 +338,10 @@ public final class RoleArenaController implements Listener, Runnable {
World world = server.getWorld(configured.orElseThrow().worldName());
Zombie boss = world == null ? null : boss(world);
if (boss != null && !boss.isDead()) {
boss.setHealth(settings.vigilanteBossHealth());
boss.teleport(toBukkit(world, configured.orElseThrow()));
boss.setTarget(null);
ArenaBossState.idle(
boss, toBukkit(world, configured.orElseThrow()),
settings.vigilanteBossHealth()
);
} else {
bossId = null;
}
@@ -349,6 +376,7 @@ public final class RoleArenaController implements Listener, Runnable {
zombie.setCustomNameVisible(true);
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());
@@ -442,29 +470,46 @@ public final class RoleArenaController implements Listener, Runnable {
}
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(
world,
arena.x() + Math.cos(angle) * radius,
arena.y() + 1.0,
arena.z() + Math.sin(angle) * radius
);
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);
int[] heightOffsets = boundaryHeightOffsets(
barrier.active(), settings.arenaBarrierHeightBlocks()
);
for (int heightOffset : heightOffsets) {
for (int index = 0; index < 48; index++) {
double angle = 2.0 * Math.PI * index / 48.0;
Location point = new Location(
world,
arena.x() + Math.cos(angle) * radius,
arena.y() + heightOffset,
arena.z() + Math.sin(angle) * radius
);
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
);
}
}
}
}
private boolean protects(Location location) {
Optional<ArenaLocation> configured = locations.location();
World world = location.getWorld();
return configured.isPresent() && world != null
&& ArenaBuildProtection.isProtected(
barrier.active(), configured.orElseThrow(),
settings.vigilanteArenaRadiusBlocks(), world.getName(),
location.getX(), location.getY(), location.getZ()
);
}
private boolean contains(ArenaLocation arena, Location location) {
World world = location.getWorld();
return world != null && ArenaGeometry.contains(
@@ -535,6 +580,18 @@ public final class RoleArenaController implements Listener, Runnable {
}
}
static int[] boundaryHeightOffsets(boolean fightActive, int wallHeight) {
if (!fightActive) {
return new int[] {1};
}
int count = (wallHeight - 2) / 3 + 1;
int[] offsets = new int[count];
for (int index = 0; index < count; index++) {
offsets[index] = 1 + index * 3;
}
return offsets;
}
static Color boundaryColor(ArenaRole role) {
if (role == null) {
return null;
+1
View File
@@ -4,6 +4,7 @@ follower-range-blocks: 50
# Shared Tyrant and Vigilante arena and boss
vigilante-arena-radius-blocks: 10
arena-barrier-height-blocks: 12
vigilante-boss-health: 80
vigilante-boss-damage: 8
vigilante-boss-armor: 10