From f0dbde17b8d2f976dc71865d6ce5a52918550d64 Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Sun, 23 Aug 2026 18:13:24 -0400 Subject: [PATCH] fix(arena): improve containment and boss idle --- design/log.md | 7 ++ .../us-012-configure-and-persist-state.md | 2 +- .../us-019-claim-vigilante-in-arena.md | 5 +- .../us-020-claim-tyrant-in-arena.md | 2 +- .../dmg/spigottyrant/ArenaBossState.java | 22 ++++ .../spigottyrant/ArenaBuildProtection.java | 20 ++++ .../dmg/spigottyrant/PluginSettings.java | 7 ++ ...renaBarrier.java => RoleArenaBarrier.java} | 13 ++- .../dmg/spigottyrant/RoleArenaController.java | 103 ++++++++++++++---- src/main/resources/config.yml | 1 + .../dmg/spigottyrant/ArenaBossStateTest.java | 35 ++++++ .../ArenaBuildProtectionTest.java | 25 +++++ .../DefaultConfigurationTest.java | 1 + .../dmg/spigottyrant/PluginSettingsTest.java | 5 + ...ierTest.java => RoleArenaBarrierTest.java} | 29 ++++- .../spigottyrant/RoleArenaControllerTest.java | 7 ++ 16 files changed, 250 insertions(+), 34 deletions(-) create mode 100644 src/main/java/games/dmg/spigottyrant/ArenaBossState.java create mode 100644 src/main/java/games/dmg/spigottyrant/ArenaBuildProtection.java rename src/main/java/games/dmg/spigottyrant/{VigilanteArenaBarrier.java => RoleArenaBarrier.java} (86%) create mode 100644 src/test/java/games/dmg/spigottyrant/ArenaBossStateTest.java create mode 100644 src/test/java/games/dmg/spigottyrant/ArenaBuildProtectionTest.java rename src/test/java/games/dmg/spigottyrant/{VigilanteArenaBarrierTest.java => RoleArenaBarrierTest.java} (61%) diff --git a/design/log.md b/design/log.md index bc4fbbf..4953ac2 100644 --- a/design/log.md +++ b/design/log.md @@ -6,6 +6,13 @@ description: Chronological record of material decisions affecting the Spigot Tyr # Spigot Tyrant Design Log +## 2026-08-23 — Arena containment and idle boss corrected + +- Corrected shared-arena containment with a validated 12-block barrier wall extending below the arena floor and active-fight block placement and breaking protection. +- Kept red or blue role particles visible at multiple wall heights during fights so challengers can see the otherwise invisible barrier from inside. +- Arena bosses now remain AI-disabled at the center until an eligible challenger enters, move normally within the arena during combat, and return healed and idle only on reset or boundary escape. +- Verified wall geometry, safe restoration, boundary visibility policy, build protection, idle/combat boss transitions, configuration validation, compiler warnings, 119 automated tests, and packaging with `./gradlew clean check jar`. + ## 2026-08-23 — Shared Tyrant and Vigilante arena succession completed - Completed US-020 and reopened related succession, inactivity, administration, configuration, and Vigilante-arena stories so non-player Tyrant replacement is earned rather than random. diff --git a/design/user-stories/us-012-configure-and-persist-state.md b/design/user-stories/us-012-configure-and-persist-state.md index 8ee8600..cfc5098 100644 --- a/design/user-stories/us-012-configure-and-persist-state.md +++ b/design/user-stories/us-012-configure-and-persist-state.md @@ -11,7 +11,7 @@ As a **server operator**, I want configurable and durable game behavior so that ## Acceptance criteria -- [x] Configuration covers ranges, durations, cooldowns, inactivity and maintenance intervals, effect levels and caps, mob restrictions, item materials and names, messages, timer behavior, and shared-role-arena boss balance. +- [x] Configuration covers ranges, durations, cooldowns, inactivity and maintenance intervals, effect levels and caps, mob restrictions, item materials and names, messages, timer behavior, shared-role-arena boss balance, and a validated barrier wall height defaulting to 12 blocks. - [x] Defaults match the approved user stories, including a 50-block Tyrant range and Follower range, seven-day opt-out, 48-hour inactivity, and one-minute role maintenance. - [x] Invalid required configuration prevents partial plugin initialization and produces a clear server log message. - [x] UUID-keyed state stores lifecycle, current and pending roles, assignments, login history, participation, reign progression, purchases, choices, cooldowns, paused time, item delivery, and captured mobs; the administrator-set arena center is durable across restarts. diff --git a/design/user-stories/us-019-claim-vigilante-in-arena.md b/design/user-stories/us-019-claim-vigilante-in-arena.md index 645652c..5d2b23c 100644 --- a/design/user-stories/us-019-claim-vigilante-in-arena.md +++ b/design/user-stories/us-019-claim-vigilante-in-arena.md @@ -17,11 +17,14 @@ As an **opted-in participant**, I want a visible one-player boss challenge for a - [x] The challenge boss defaults to an armored, 80-health enhanced zombie, does not burn in sunlight, and remains within a default 10-block-radius arena. - [x] All online opted-in players are told when the role opens and receive the arena world and coordinates; opted-in players receive the same notice when they log in while it remains open, and opted-out players do not. - [x] Exactly one eligible opted-in non-Tyrant player may enter as challenger, including when entry is attempted by walking or teleportation. -- [x] Starting a fight replaces the passive particle boundary with temporary actual `BARRIER` blocks that are removed safely without permanently changing existing blocks. +- [x] Starting a fight builds temporary actual `BARRIER` blocks from one block below the arena floor through a default 12-block wall height, without permanently changing existing blocks. +- [x] Role-colored boundary particles remain visible from inside and outside an active fight at multiple wall heights. - [x] Any non-challenger who enters or respawns inside the arena is moved safely outside it. - [x] Existing mobs in the arena and mobs that spawn or enter it are removed while preserving the arena boss. +- [x] With no challenger, the boss remains AI-disabled at the arena center; entry enables its AI and target, while reset restores full health, center position, no target, and disabled AI. - [x] The boss attacks only the active challenger, and only that challenger can damage the boss. - [x] Challenger death, disconnect, or leaving by any mechanism ends the fight and resets the boss to full health at its spawn; death counts as leaving and the same player may retry immediately. +- [x] Block placement and breaking are denied inside the arena during an active fight so players cannot build over or modify the boundary. - [x] Killing the boss assigns the challenger as Vigilante, removes the boss and barriers, and restores the passive particle boundary. - [x] Pausing, resetting, restarting, reloading, and repeated or concurrent events cannot duplicate the boss, barriers, challenger, or Vigilante assignment. - [x] If the arena is not configured when a challenge should open, the Vigilante remains vacant and administrators receive a clear warning. diff --git a/design/user-stories/us-020-claim-tyrant-in-arena.md b/design/user-stories/us-020-claim-tyrant-in-arena.md index ff9322d..d392bf0 100644 --- a/design/user-stories/us-020-claim-tyrant-in-arena.md +++ b/design/user-stories/us-020-claim-tyrant-in-arena.md @@ -15,7 +15,7 @@ As an **opted-in participant**, I want to earn a vacant Tyrant role through the - [x] Tyrant death without a player killer, Tyrant inactivity, and Tyrant relinquishment end the reign, clear all role-side assignments, and open the shared arena with no active or pending Tyrant. - [x] The shared arena opens only one role challenge at a time: a vacant Tyrant takes priority, and the Vigilante challenge can open only after a Tyrant becomes active. - [x] The passive arena boundary uses red particles for an open Tyrant challenge, blue particles for an open Vigilante challenge, and a neutral marker while neither role challenge is open. -- [x] The Tyrant challenge uses the existing configured arena, boss balance, mob exclusion, one-challenger barrier, containment, reset, notification, and missing-location rules. +- [x] The Tyrant challenge uses the existing configured arena, boss balance, tall visible one-challenger barrier, block-change protection, idle-center boss behavior, containment, reset, notification, and missing-location rules. - [x] An eligible opted-in participant, including the former Tyrant, may challenge for the vacant Tyrant role; an active Tyrant cannot enter any arena fight. - [x] Defeating the Tyrant challenge boss assigns the challenger as Tyrant with level zero, no purchases, and exactly one starting unlock choice. - [x] Completing the Tyrant challenge immediately permits the blue Vigilante challenge to open at the same location when the Vigilante role is vacant. diff --git a/src/main/java/games/dmg/spigottyrant/ArenaBossState.java b/src/main/java/games/dmg/spigottyrant/ArenaBossState.java new file mode 100644 index 0000000..9184d67 --- /dev/null +++ b/src/main/java/games/dmg/spigottyrant/ArenaBossState.java @@ -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); + } +} diff --git a/src/main/java/games/dmg/spigottyrant/ArenaBuildProtection.java b/src/main/java/games/dmg/spigottyrant/ArenaBuildProtection.java new file mode 100644 index 0000000..cacdb1b --- /dev/null +++ b/src/main/java/games/dmg/spigottyrant/ArenaBuildProtection.java @@ -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 + ); + } +} diff --git a/src/main/java/games/dmg/spigottyrant/PluginSettings.java b/src/main/java/games/dmg/spigottyrant/PluginSettings.java index 2e12158..ac30fd6 100644 --- a/src/main/java/games/dmg/spigottyrant/PluginSettings.java +++ b/src/main/java/games/dmg/spigottyrant/PluginSettings.java @@ -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), diff --git a/src/main/java/games/dmg/spigottyrant/VigilanteArenaBarrier.java b/src/main/java/games/dmg/spigottyrant/RoleArenaBarrier.java similarity index 86% rename from src/main/java/games/dmg/spigottyrant/VigilanteArenaBarrier.java rename to src/main/java/games/dmg/spigottyrant/RoleArenaBarrier.java index 2efff0e..4da1c5d 100644 --- a/src/main/java/games/dmg/spigottyrant/VigilanteArenaBarrier.java +++ b/src/main/java/games/dmg/spigottyrant/RoleArenaBarrier.java @@ -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 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())) { diff --git a/src/main/java/games/dmg/spigottyrant/RoleArenaController.java b/src/main/java/games/dmg/spigottyrant/RoleArenaController.java index 3ade5c5..dc4a7e5 100644 --- a/src/main/java/games/dmg/spigottyrant/RoleArenaController.java +++ b/src/main/java/games/dmg/spigottyrant/RoleArenaController.java @@ -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 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 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; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index be81578..364f75d 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -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 diff --git a/src/test/java/games/dmg/spigottyrant/ArenaBossStateTest.java b/src/test/java/games/dmg/spigottyrant/ArenaBossStateTest.java new file mode 100644 index 0000000..574f198 --- /dev/null +++ b/src/test/java/games/dmg/spigottyrant/ArenaBossStateTest.java @@ -0,0 +1,35 @@ +package games.dmg.spigottyrant; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.entity.Zombie; +import org.junit.jupiter.api.Test; + +final class ArenaBossStateTest { + @Test + void idleBossIsCenteredHealedUntargetedAndFrozen() { + Zombie boss = mock(Zombie.class); + Location center = mock(Location.class); + + ArenaBossState.idle(boss, center, 80.0); + + verify(boss).setAI(false); + verify(boss).setTarget(null); + verify(boss).setHealth(80.0); + verify(boss).teleport(center); + } + + @Test + void challengerEntryEnablesBossAndSetsTarget() { + Zombie boss = mock(Zombie.class); + Player challenger = mock(Player.class); + + ArenaBossState.engage(boss, challenger); + + verify(boss).setAI(true); + verify(boss).setTarget(challenger); + } +} diff --git a/src/test/java/games/dmg/spigottyrant/ArenaBuildProtectionTest.java b/src/test/java/games/dmg/spigottyrant/ArenaBuildProtectionTest.java new file mode 100644 index 0000000..0b70f54 --- /dev/null +++ b/src/test/java/games/dmg/spigottyrant/ArenaBuildProtectionTest.java @@ -0,0 +1,25 @@ +package games.dmg.spigottyrant; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +final class ArenaBuildProtectionTest { + private static final ArenaLocation CENTER = new ArenaLocation( + "world", 0.0, 64.0, 0.0, 0.0F, 0.0F + ); + + @Test + void protectsOnlyInsideConfiguredArenaDuringFight() { + assertTrue(ArenaBuildProtection.isProtected( + true, CENTER, 10.0, "world", 5.0, 70.0, 5.0 + )); + assertFalse(ArenaBuildProtection.isProtected( + false, CENTER, 10.0, "world", 5.0, 70.0, 5.0 + )); + assertFalse(ArenaBuildProtection.isProtected( + true, CENTER, 10.0, "world", 11.0, 70.0, 0.0 + )); + } +} diff --git a/src/test/java/games/dmg/spigottyrant/DefaultConfigurationTest.java b/src/test/java/games/dmg/spigottyrant/DefaultConfigurationTest.java index 41d5cb4..75ce2b0 100644 --- a/src/test/java/games/dmg/spigottyrant/DefaultConfigurationTest.java +++ b/src/test/java/games/dmg/spigottyrant/DefaultConfigurationTest.java @@ -21,6 +21,7 @@ final class DefaultConfigurationTest { assertEquals(50.0, settings.tyrantRangeBlocks()); assertEquals(Duration.ofDays(7), settings.optOutDuration()); assertEquals(Duration.ofMinutes(1), settings.roleMaintenanceInterval()); + assertEquals(12, settings.arenaBarrierHeightBlocks()); assertEquals("FISHING_ROD", settings.tamerItem().material()); } diff --git a/src/test/java/games/dmg/spigottyrant/PluginSettingsTest.java b/src/test/java/games/dmg/spigottyrant/PluginSettingsTest.java index 55a6314..c44533d 100644 --- a/src/test/java/games/dmg/spigottyrant/PluginSettingsTest.java +++ b/src/test/java/games/dmg/spigottyrant/PluginSettingsTest.java @@ -24,6 +24,7 @@ final class PluginSettingsTest { assertEquals(Duration.ofMinutes(10), settings.fixerEffectDuration()); assertEquals(Duration.ofHours(24), settings.rosterIntelligenceCooldown()); assertEquals(10.0, settings.vigilanteArenaRadiusBlocks()); + assertEquals(12, settings.arenaBarrierHeightBlocks()); assertEquals(80.0, settings.vigilanteBossHealth()); assertEquals(8.0, settings.vigilanteBossDamage()); assertEquals(10.0, settings.vigilanteBossArmor()); @@ -84,6 +85,10 @@ final class PluginSettingsTest { IllegalArgumentException.class, () -> PluginSettings.from(Map.of("vigilante-arena-radius-blocks", 0)) ); + assertThrows( + IllegalArgumentException.class, + () -> PluginSettings.from(Map.of("arena-barrier-height-blocks", 2)) + ); assertThrows( IllegalArgumentException.class, () -> PluginSettings.from(Map.of("vigilante-boss-health", Double.NaN)) diff --git a/src/test/java/games/dmg/spigottyrant/VigilanteArenaBarrierTest.java b/src/test/java/games/dmg/spigottyrant/RoleArenaBarrierTest.java similarity index 61% rename from src/test/java/games/dmg/spigottyrant/VigilanteArenaBarrierTest.java rename to src/test/java/games/dmg/spigottyrant/RoleArenaBarrierTest.java index 7a3bc8f..c0c811a 100644 --- a/src/test/java/games/dmg/spigottyrant/VigilanteArenaBarrierTest.java +++ b/src/test/java/games/dmg/spigottyrant/RoleArenaBarrierTest.java @@ -1,5 +1,6 @@ package games.dmg.spigottyrant; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeastOnce; @@ -8,14 +9,16 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.List; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; -final class VigilanteArenaBarrierTest { +final class RoleArenaBarrierTest { private static final ArenaLocation CENTER = new ArenaLocation( "world", 0.0, 64.0, 0.0, 0.0F, 0.0F ); @@ -28,7 +31,7 @@ final class VigilanteArenaBarrierTest { when(solid.getLocation()).thenReturn(new Location(world, 1.0, 64.0, 0.0)); when(solid.getType()).thenReturn(Material.CHEST); - new VigilanteArenaBarrier().build(world, CENTER, 10.0); + new RoleArenaBarrier().build(world, CENTER, 10.0, 12); verify(solid, never()).setType(Material.BARRIER, false); } @@ -45,12 +48,30 @@ final class VigilanteArenaBarrierTest { when(air.getType()).thenReturn(Material.AIR); when(air.getBlockData()).thenReturn(original); when(original.clone()).thenReturn(snapshot); - VigilanteArenaBarrier barrier = new VigilanteArenaBarrier(); + RoleArenaBarrier barrier = new RoleArenaBarrier(); - barrier.build(world, CENTER, 10.0); + barrier.build(world, CENTER, 10.0, 12); barrier.clear(); verify(air, atLeastOnce()).setType(Material.BARRIER, false); verify(air).setBlockData(snapshot, false); } + + @Test + void wallExtendsOneBelowFloorForTwelveBlocks() { + World world = mock(World.class); + 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)); + when(solid.getType()).thenReturn(Material.CHEST); + RoleArenaBarrier barrier = new RoleArenaBarrier(); + + barrier.build(world, CENTER, 10.0, 12); + + ArgumentCaptor y = ArgumentCaptor.forClass(Integer.class); + verify(world, atLeastOnce()).getBlockAt(anyInt(), y.capture(), anyInt()); + List values = y.getAllValues(); + assertEquals(63, values.stream().mapToInt(Integer::intValue).min().orElseThrow()); + assertEquals(74, values.stream().mapToInt(Integer::intValue).max().orElseThrow()); + } } diff --git a/src/test/java/games/dmg/spigottyrant/RoleArenaControllerTest.java b/src/test/java/games/dmg/spigottyrant/RoleArenaControllerTest.java index 0cf46ba..e609856 100644 --- a/src/test/java/games/dmg/spigottyrant/RoleArenaControllerTest.java +++ b/src/test/java/games/dmg/spigottyrant/RoleArenaControllerTest.java @@ -1,6 +1,7 @@ package games.dmg.spigottyrant; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.Mockito.mock; @@ -29,6 +30,12 @@ final class RoleArenaControllerTest { assertEquals(Color.RED, RoleArenaController.boundaryColor(ArenaRole.TYRANT)); assertEquals(Color.BLUE, RoleArenaController.boundaryColor(ArenaRole.VIGILANTE)); assertNull(RoleArenaController.boundaryColor(null)); + assertArrayEquals( + new int[] {1}, RoleArenaController.boundaryHeightOffsets(false, 12) + ); + assertArrayEquals( + new int[] {1, 4, 7, 10}, RoleArenaController.boundaryHeightOffsets(true, 12) + ); } @Test