fix(arena): improve containment and boss idle
This commit is contained in:
@@ -6,6 +6,13 @@ description: Chronological record of material decisions affecting the Spigot Tyr
|
|||||||
|
|
||||||
# Spigot Tyrant Design Log
|
# 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
|
## 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.
|
- Completed US-020 and reopened related succession, inactivity, administration, configuration, and Vigilante-arena stories so non-player Tyrant replacement is earned rather than random.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ As a **server operator**, I want configurable and durable game behavior so that
|
|||||||
|
|
||||||
## Acceptance criteria
|
## 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] 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] 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.
|
- [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.
|
||||||
|
|||||||
@@ -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] 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] 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] 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] 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] 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] 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] 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] 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] 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.
|
- [x] If the arena is not configured when a challenge should open, the Vigilante remains vacant and administrators receive a clear warning.
|
||||||
|
|||||||
@@ -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] 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 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 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] 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] 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.
|
- [x] Completing the Tyrant challenge immediately permits the blue Vigilante challenge to open at the same location when the Vigilante role is vacant.
|
||||||
|
|||||||
@@ -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 tyrantRangeBlocks,
|
||||||
double followerRangeBlocks,
|
double followerRangeBlocks,
|
||||||
double vigilanteArenaRadiusBlocks,
|
double vigilanteArenaRadiusBlocks,
|
||||||
|
int arenaBarrierHeightBlocks,
|
||||||
double vigilanteBossHealth,
|
double vigilanteBossHealth,
|
||||||
double vigilanteBossDamage,
|
double vigilanteBossDamage,
|
||||||
double vigilanteBossArmor,
|
double vigilanteBossArmor,
|
||||||
@@ -53,6 +54,11 @@ public record PluginSettings(
|
|||||||
requireFiniteRange(
|
requireFiniteRange(
|
||||||
vigilanteArenaRadiusBlocks, 1.0, 512.0, "vigilante-arena-radius-blocks"
|
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(vigilanteBossHealth, 1.0, 1024.0, "vigilante-boss-health");
|
||||||
requireFiniteRange(vigilanteBossDamage, 0.1, 2048.0, "vigilante-boss-damage");
|
requireFiniteRange(vigilanteBossDamage, 0.1, 2048.0, "vigilante-boss-damage");
|
||||||
requireFiniteRange(vigilanteBossArmor, 0.0, 30.0, "vigilante-boss-armor");
|
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, "tyrant-range-blocks", 50.0),
|
||||||
decimal(values, "follower-range-blocks", 50.0),
|
decimal(values, "follower-range-blocks", 50.0),
|
||||||
decimal(values, "vigilante-arena-radius-blocks", 10.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-health", 80.0),
|
||||||
decimal(values, "vigilante-boss-damage", 8.0),
|
decimal(values, "vigilante-boss-damage", 8.0),
|
||||||
decimal(values, "vigilante-boss-armor", 10.0),
|
decimal(values, "vigilante-boss-armor", 10.0),
|
||||||
|
|||||||
+9
-4
@@ -8,22 +8,27 @@ import org.bukkit.World;
|
|||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
final class VigilanteArenaBarrier {
|
final class RoleArenaBarrier {
|
||||||
private final Map<Location, BlockData> replaced = new LinkedHashMap<>();
|
private final Map<Location, BlockData> replaced = new LinkedHashMap<>();
|
||||||
private boolean built;
|
private boolean built;
|
||||||
|
|
||||||
void build(World world, ArenaLocation center, double radius) {
|
void build(
|
||||||
|
World world,
|
||||||
|
ArenaLocation center,
|
||||||
|
double radius,
|
||||||
|
int wallHeight
|
||||||
|
) {
|
||||||
if (built) {
|
if (built) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
built = true;
|
built = true;
|
||||||
int samples = Math.max(64, (int) Math.ceil(2.0 * Math.PI * radius * 2.0));
|
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++) {
|
for (int index = 0; index < samples; index++) {
|
||||||
double angle = 2.0 * Math.PI * index / samples;
|
double angle = 2.0 * Math.PI * index / samples;
|
||||||
int x = (int) Math.floor(center.x() + Math.cos(angle) * radius);
|
int x = (int) Math.floor(center.x() + Math.cos(angle) * radius);
|
||||||
int z = (int) Math.floor(center.z() + Math.sin(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);
|
Block block = world.getBlockAt(x, y, z);
|
||||||
Location location = block.getLocation();
|
Location location = block.getLocation();
|
||||||
if (replaced.containsKey(location) || !isAir(block.getType())) {
|
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.EntityDeathEvent;
|
||||||
import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
|
import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
|
||||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
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.PlayerJoinEvent;
|
||||||
import org.bukkit.event.player.PlayerMoveEvent;
|
import org.bukkit.event.player.PlayerMoveEvent;
|
||||||
import org.bukkit.event.player.PlayerQuitEvent;
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
@@ -42,7 +44,7 @@ public final class RoleArenaController implements Listener, Runnable {
|
|||||||
new VigilanteArenaSuccessionService();
|
new VigilanteArenaSuccessionService();
|
||||||
private final TyrantArenaSuccessionService tyrantSuccession =
|
private final TyrantArenaSuccessionService tyrantSuccession =
|
||||||
new TyrantArenaSuccessionService();
|
new TyrantArenaSuccessionService();
|
||||||
private final VigilanteArenaBarrier barrier = new VigilanteArenaBarrier();
|
private final RoleArenaBarrier barrier = new RoleArenaBarrier();
|
||||||
private final Set<UUID> relocating = new HashSet<>();
|
private final Set<UUID> relocating = new HashSet<>();
|
||||||
private UUID bossId;
|
private UUID bossId;
|
||||||
private UUID challengerId;
|
private UUID challengerId;
|
||||||
@@ -121,10 +123,13 @@ public final class RoleArenaController implements Listener, Runnable {
|
|||||||
)) {
|
)) {
|
||||||
if (challengerId != null) {
|
if (challengerId != null) {
|
||||||
resetFight();
|
resetFight();
|
||||||
|
} else {
|
||||||
|
ArenaBossState.idle(
|
||||||
|
boss, toBukkit(world, arena), settings.vigilanteBossHealth()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
boss.setTarget(null);
|
|
||||||
} else {
|
} else {
|
||||||
boss.setTarget(challenger);
|
ArenaBossState.engage(boss, challenger);
|
||||||
}
|
}
|
||||||
for (Player player : world.getPlayers()) {
|
for (Player player : world.getPlayers()) {
|
||||||
enforcePosition(player, null, player.getLocation());
|
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)
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
public void onCombust(EntityCombustEvent event) {
|
public void onCombust(EntityCombustEvent event) {
|
||||||
if (event.getEntity().getUniqueId().equals(bossId)) {
|
if (event.getEntity().getUniqueId().equals(bossId)) {
|
||||||
@@ -274,12 +297,15 @@ public final class RoleArenaController implements Listener, Runnable {
|
|||||||
challengerId = player.getUniqueId();
|
challengerId = player.getUniqueId();
|
||||||
World world = server.getWorld(arena.worldName());
|
World world = server.getWorld(arena.worldName());
|
||||||
if (world != null) {
|
if (world != null) {
|
||||||
barrier.build(world, arena, settings.vigilanteArenaRadiusBlocks());
|
barrier.build(
|
||||||
|
world, arena, settings.vigilanteArenaRadiusBlocks(),
|
||||||
|
settings.arenaBarrierHeightBlocks()
|
||||||
|
);
|
||||||
Zombie boss = boss(world);
|
Zombie boss = boss(world);
|
||||||
if (boss != null) {
|
if (boss != null) {
|
||||||
boss.setHealth(settings.vigilanteBossHealth());
|
boss.setHealth(settings.vigilanteBossHealth());
|
||||||
boss.teleport(toBukkit(world, arena));
|
boss.teleport(toBukkit(world, arena));
|
||||||
boss.setTarget(player);
|
ArenaBossState.engage(boss, player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
player.sendMessage(ChatColor.GOLD + "Defeat the arena boss to become "
|
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());
|
World world = server.getWorld(configured.orElseThrow().worldName());
|
||||||
Zombie boss = world == null ? null : boss(world);
|
Zombie boss = world == null ? null : boss(world);
|
||||||
if (boss != null && !boss.isDead()) {
|
if (boss != null && !boss.isDead()) {
|
||||||
boss.setHealth(settings.vigilanteBossHealth());
|
ArenaBossState.idle(
|
||||||
boss.teleport(toBukkit(world, configured.orElseThrow()));
|
boss, toBukkit(world, configured.orElseThrow()),
|
||||||
boss.setTarget(null);
|
settings.vigilanteBossHealth()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
bossId = null;
|
bossId = null;
|
||||||
}
|
}
|
||||||
@@ -349,6 +376,7 @@ public final class RoleArenaController implements Listener, Runnable {
|
|||||||
zombie.setCustomNameVisible(true);
|
zombie.setCustomNameVisible(true);
|
||||||
zombie.setRemoveWhenFarAway(false);
|
zombie.setRemoveWhenFarAway(false);
|
||||||
zombie.setCanPickupItems(false);
|
zombie.setCanPickupItems(false);
|
||||||
|
zombie.setAI(false);
|
||||||
zombie.addScoreboardTag(BOSS_TAG);
|
zombie.addScoreboardTag(BOSS_TAG);
|
||||||
setAttribute(zombie, Attribute.MAX_HEALTH, settings.vigilanteBossHealth());
|
setAttribute(zombie, Attribute.MAX_HEALTH, settings.vigilanteBossHealth());
|
||||||
setAttribute(zombie, Attribute.ATTACK_DAMAGE, settings.vigilanteBossDamage());
|
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) {
|
private void drawBoundary(World world, ArenaLocation arena, ArenaRole role) {
|
||||||
if (barrier.active()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
double radius = settings.vigilanteArenaRadiusBlocks();
|
double radius = settings.vigilanteArenaRadiusBlocks();
|
||||||
Color color = boundaryColor(role);
|
Color color = boundaryColor(role);
|
||||||
Particle.DustOptions dust = color == null
|
Particle.DustOptions dust = color == null
|
||||||
? null : new Particle.DustOptions(color, 1.25F);
|
? null : new Particle.DustOptions(color, 1.25F);
|
||||||
for (int index = 0; index < 48; index++) {
|
int[] heightOffsets = boundaryHeightOffsets(
|
||||||
double angle = 2.0 * Math.PI * index / 48.0;
|
barrier.active(), settings.arenaBarrierHeightBlocks()
|
||||||
Location point = new Location(
|
);
|
||||||
world,
|
for (int heightOffset : heightOffsets) {
|
||||||
arena.x() + Math.cos(angle) * radius,
|
for (int index = 0; index < 48; index++) {
|
||||||
arena.y() + 1.0,
|
double angle = 2.0 * Math.PI * index / 48.0;
|
||||||
arena.z() + Math.sin(angle) * radius
|
Location point = new Location(
|
||||||
);
|
world,
|
||||||
if (dust == null) {
|
arena.x() + Math.cos(angle) * radius,
|
||||||
world.spawnParticle(Particle.END_ROD, point, 1, 0.0, 0.0, 0.0, 0.0);
|
arena.y() + heightOffset,
|
||||||
} else {
|
arena.z() + Math.sin(angle) * radius
|
||||||
world.spawnParticle(Particle.DUST, point, 1, 0.0, 0.0, 0.0, 0.0, dust);
|
);
|
||||||
|
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) {
|
private boolean contains(ArenaLocation arena, Location location) {
|
||||||
World world = location.getWorld();
|
World world = location.getWorld();
|
||||||
return world != null && ArenaGeometry.contains(
|
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) {
|
static Color boundaryColor(ArenaRole role) {
|
||||||
if (role == null) {
|
if (role == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ follower-range-blocks: 50
|
|||||||
|
|
||||||
# Shared Tyrant and Vigilante arena and boss
|
# Shared Tyrant and Vigilante arena and boss
|
||||||
vigilante-arena-radius-blocks: 10
|
vigilante-arena-radius-blocks: 10
|
||||||
|
arena-barrier-height-blocks: 12
|
||||||
vigilante-boss-health: 80
|
vigilante-boss-health: 80
|
||||||
vigilante-boss-damage: 8
|
vigilante-boss-damage: 8
|
||||||
vigilante-boss-armor: 10
|
vigilante-boss-armor: 10
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ final class DefaultConfigurationTest {
|
|||||||
assertEquals(50.0, settings.tyrantRangeBlocks());
|
assertEquals(50.0, settings.tyrantRangeBlocks());
|
||||||
assertEquals(Duration.ofDays(7), settings.optOutDuration());
|
assertEquals(Duration.ofDays(7), settings.optOutDuration());
|
||||||
assertEquals(Duration.ofMinutes(1), settings.roleMaintenanceInterval());
|
assertEquals(Duration.ofMinutes(1), settings.roleMaintenanceInterval());
|
||||||
|
assertEquals(12, settings.arenaBarrierHeightBlocks());
|
||||||
assertEquals("FISHING_ROD", settings.tamerItem().material());
|
assertEquals("FISHING_ROD", settings.tamerItem().material());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ final class PluginSettingsTest {
|
|||||||
assertEquals(Duration.ofMinutes(10), settings.fixerEffectDuration());
|
assertEquals(Duration.ofMinutes(10), settings.fixerEffectDuration());
|
||||||
assertEquals(Duration.ofHours(24), settings.rosterIntelligenceCooldown());
|
assertEquals(Duration.ofHours(24), settings.rosterIntelligenceCooldown());
|
||||||
assertEquals(10.0, settings.vigilanteArenaRadiusBlocks());
|
assertEquals(10.0, settings.vigilanteArenaRadiusBlocks());
|
||||||
|
assertEquals(12, settings.arenaBarrierHeightBlocks());
|
||||||
assertEquals(80.0, settings.vigilanteBossHealth());
|
assertEquals(80.0, settings.vigilanteBossHealth());
|
||||||
assertEquals(8.0, settings.vigilanteBossDamage());
|
assertEquals(8.0, settings.vigilanteBossDamage());
|
||||||
assertEquals(10.0, settings.vigilanteBossArmor());
|
assertEquals(10.0, settings.vigilanteBossArmor());
|
||||||
@@ -84,6 +85,10 @@ final class PluginSettingsTest {
|
|||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> PluginSettings.from(Map.of("vigilante-arena-radius-blocks", 0))
|
() -> PluginSettings.from(Map.of("vigilante-arena-radius-blocks", 0))
|
||||||
);
|
);
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> PluginSettings.from(Map.of("arena-barrier-height-blocks", 2))
|
||||||
|
);
|
||||||
assertThrows(
|
assertThrows(
|
||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> PluginSettings.from(Map.of("vigilante-boss-health", Double.NaN))
|
() -> PluginSettings.from(Map.of("vigilante-boss-health", Double.NaN))
|
||||||
|
|||||||
+25
-4
@@ -1,5 +1,6 @@
|
|||||||
package games.dmg.spigottyrant;
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.Mockito.atLeastOnce;
|
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.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
final class VigilanteArenaBarrierTest {
|
final class RoleArenaBarrierTest {
|
||||||
private static final ArenaLocation CENTER = new ArenaLocation(
|
private static final ArenaLocation CENTER = new ArenaLocation(
|
||||||
"world", 0.0, 64.0, 0.0, 0.0F, 0.0F
|
"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.getLocation()).thenReturn(new Location(world, 1.0, 64.0, 0.0));
|
||||||
when(solid.getType()).thenReturn(Material.CHEST);
|
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);
|
verify(solid, never()).setType(Material.BARRIER, false);
|
||||||
}
|
}
|
||||||
@@ -45,12 +48,30 @@ final class VigilanteArenaBarrierTest {
|
|||||||
when(air.getType()).thenReturn(Material.AIR);
|
when(air.getType()).thenReturn(Material.AIR);
|
||||||
when(air.getBlockData()).thenReturn(original);
|
when(air.getBlockData()).thenReturn(original);
|
||||||
when(original.clone()).thenReturn(snapshot);
|
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();
|
barrier.clear();
|
||||||
|
|
||||||
verify(air, atLeastOnce()).setType(Material.BARRIER, false);
|
verify(air, atLeastOnce()).setType(Material.BARRIER, false);
|
||||||
verify(air).setBlockData(snapshot, 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<Integer> y = ArgumentCaptor.forClass(Integer.class);
|
||||||
|
verify(world, atLeastOnce()).getBlockAt(anyInt(), y.capture(), anyInt());
|
||||||
|
List<Integer> values = y.getAllValues();
|
||||||
|
assertEquals(63, values.stream().mapToInt(Integer::intValue).min().orElseThrow());
|
||||||
|
assertEquals(74, values.stream().mapToInt(Integer::intValue).max().orElseThrow());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package games.dmg.spigottyrant;
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.mockito.ArgumentMatchers.contains;
|
import static org.mockito.ArgumentMatchers.contains;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -29,6 +30,12 @@ final class RoleArenaControllerTest {
|
|||||||
assertEquals(Color.RED, RoleArenaController.boundaryColor(ArenaRole.TYRANT));
|
assertEquals(Color.RED, RoleArenaController.boundaryColor(ArenaRole.TYRANT));
|
||||||
assertEquals(Color.BLUE, RoleArenaController.boundaryColor(ArenaRole.VIGILANTE));
|
assertEquals(Color.BLUE, RoleArenaController.boundaryColor(ArenaRole.VIGILANTE));
|
||||||
assertNull(RoleArenaController.boundaryColor(null));
|
assertNull(RoleArenaController.boundaryColor(null));
|
||||||
|
assertArrayEquals(
|
||||||
|
new int[] {1}, RoleArenaController.boundaryHeightOffsets(false, 12)
|
||||||
|
);
|
||||||
|
assertArrayEquals(
|
||||||
|
new int[] {1, 4, 7, 10}, RoleArenaController.boundaryHeightOffsets(true, 12)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user