feat(border): add base boundary particles
Release / release (push) Successful in 2m18s
CI / build (push) Successful in 1m1s

This commit is contained in:
dmg
2026-08-10 22:12:16 -04:00
parent 4278e08bf3
commit 26aeb8c416
18 changed files with 458 additions and 27 deletions
@@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.time.Instant;
import java.util.UUID;
import org.junit.jupiter.api.Test;
@@ -36,6 +37,22 @@ final class AdminProgressionServiceTest {
assertFalse(updated.visitorsEnabled());
}
@Test
void loweringBaseDisablesBorderVisualization() {
UUID playerId = UUID.randomUUID();
PlayerState player = PlayerState.newPlayer(playerId, "Alex")
.withAdministrativeLevels(1, 0, 0, 0, 0, false, false, false)
.withBase(
new BaseLocation(UUID.randomUUID(), "world", 0, 64, 0, 0, 0),
Instant.EPOCH
)
.withBorderEnabled(true);
PlayerState updated = service.setLevel(player, ProgressionPath.BASE, 0);
assertFalse(updated.borderEnabled());
}
@Test
void rejectsUnknownLevel() {
PlayerState player = PlayerState.newPlayer(UUID.randomUUID(), "Alex");
@@ -0,0 +1,92 @@
package games.dmg.spigotbase;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
final class BaseBorderControllerTest {
@Test
void doesNotRenderOutsideTheBaseVerticalRange() {
UUID playerId = UUID.randomUUID();
UUID worldId = UUID.randomUUID();
PlayerState state = PlayerState.newPlayer(playerId, "Builder")
.withAdministrativeLevels(1, 0, 0, 0, 0, false, false, false)
.withBase(new BaseLocation(worldId, "world", 0, 64, 0, 0, 0), Instant.EPOCH)
.withBorderEnabled(true);
World world = mock(World.class);
when(world.getUID()).thenReturn(worldId);
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId);
when(player.getName()).thenReturn("Builder");
when(player.getWorld()).thenReturn(world);
when(player.getLocation()).thenReturn(new Location(world, 10.5, 100.0, 0.5));
Server server = mock(Server.class);
doReturn(java.util.List.of(player)).when(server).getOnlinePlayers();
BaseStateManager stateManager = mock(BaseStateManager.class);
when(stateManager.player(playerId, "Builder")).thenReturn(state);
new BaseBorderController(
server,
stateManager,
new BaseBoundsService(PluginSettings.from(Map.of()))
).run();
verify(player, never()).spawnParticle(
eq(Particle.END_ROD),
any(Location.class),
eq(1),
eq(0.0), eq(0.0), eq(0.0), eq(0.0)
);
}
@Test
void ownerSeesParticlesAtCurrentHeightNearEnabledBorder() {
UUID playerId = UUID.randomUUID();
UUID worldId = UUID.randomUUID();
PlayerState state = PlayerState.newPlayer(playerId, "Builder")
.withAdministrativeLevels(1, 0, 0, 0, 0, false, false, false)
.withBase(new BaseLocation(worldId, "world", 0, 64, 0, 0, 0), Instant.EPOCH)
.withBorderEnabled(true);
World world = mock(World.class);
when(world.getUID()).thenReturn(worldId);
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId);
when(player.getName()).thenReturn("Builder");
when(player.getWorld()).thenReturn(world);
when(player.getLocation()).thenReturn(new Location(world, 10.5, 70.0, 0.5));
Server server = mock(Server.class);
doReturn(java.util.List.of(player)).when(server).getOnlinePlayers();
BaseStateManager stateManager = mock(BaseStateManager.class);
when(stateManager.player(playerId, "Builder")).thenReturn(state);
new BaseBorderController(
server,
stateManager,
new BaseBoundsService(PluginSettings.from(Map.of()))
).run();
verify(player, atLeastOnce()).spawnParticle(
eq(Particle.END_ROD),
any(Location.class),
eq(1),
eq(0.0), eq(0.0), eq(0.0), eq(0.0)
);
}
}
@@ -0,0 +1,27 @@
package games.dmg.spigotbase;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import org.junit.jupiter.api.Test;
final class BaseBorderGeometryTest {
@Test
void producesBoundedPointsOnTheCurrentCircularBoundary() {
List<BaseBorderGeometry.Point> points = BaseBorderGeometry.visiblePoints(
0.5, 0.5, 150, 150.5, 0.5
);
assertFalse(points.isEmpty());
assertTrue(points.size() <= BaseBorderGeometry.MAX_PARTICLES);
assertTrue(points.stream().allMatch(point ->
Math.abs(Math.hypot(point.x() - 0.5, point.z() - 0.5) - 150.0) < 0.0001
));
}
@Test
void omitsParticlesWhenPlayerIsFarFromBoundary() {
assertTrue(BaseBorderGeometry.visiblePoints(0.5, 0.5, 10, 100.5, 0.5).isEmpty());
}
}
@@ -24,6 +24,21 @@ import org.bukkit.inventory.PlayerInventory;
import org.junit.jupiter.api.Test;
final class BaseSettingsCommandTest {
@Test
void borderEnableIsIdempotent() {
UUID playerId = UUID.randomUUID();
PlayerState current = PlayerState.newPlayer(playerId, "Builder")
.withAdministrativeLevels(1, 0, 0, 0, 0, false, false, false)
.withBase(
new BaseLocation(UUID.randomUUID(), "world", 0, 64, 0, 0, 0),
Instant.EPOCH
);
CommandResult result = execute(current, "border", "enable");
assertTrue(result.updated().borderEnabled());
}
@Test
void invalidArgumentsShowUsage() {
UUID playerId = UUID.randomUUID();
@@ -58,7 +73,7 @@ final class BaseSettingsCommandTest {
List.of("allowed"),
command.onTabComplete(null, null, "basesettings", new String[] {"visitors", "a"})
);
for (String setting : List.of("navigation", "flight", "bossbar")) {
for (String setting : List.of("navigation", "flight", "border", "bossbar")) {
assertEquals(
List.of("disable"),
command.onTabComplete(null, null, "basesettings", new String[] {setting, "d"})
@@ -75,7 +90,7 @@ final class BaseSettingsCommandTest {
);
assertEquals(
List.of("status", "upgrade", "visitors", "navigation", "flight", "bossbar"),
List.of("status", "upgrade", "visitors", "navigation", "flight", "border", "bossbar"),
command.onTabComplete(null, null, "basesettings", new String[] {""})
);
}
@@ -194,6 +209,7 @@ final class BaseSettingsCommandTest {
message.contains("Settings: visitors=")
&& message.contains("navigation=")
&& message.contains("flight=")
&& message.contains("border=")
&& message.contains("bossbar=")
)
);
@@ -1,13 +1,31 @@
package games.dmg.spigotbase;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.Test;
final class PlayerStateTest {
@Test
void borderVisualizationIsDisabledByDefaultAndCanBeEnabled() {
UUID playerId = UUID.randomUUID();
PlayerState player = PlayerState.newPlayer(playerId, "Alex");
PlayerState established = player
.withAdministrativeLevels(1, 0, 0, 0, 0, false, false, false)
.withBase(
new BaseLocation(UUID.randomUUID(), "world", 0, 64, 0, 0, 0),
Instant.EPOCH
);
assertFalse(player.borderEnabled());
assertTrue(established.withBorderEnabled(true).borderEnabled());
}
@Test
void rejectsLevelsOutsideKnownRanges() {
UUID playerId = UUID.randomUUID();
@@ -28,7 +28,8 @@ final class YamlBaseStateRepositoryTest {
true, true, false, true,
Optional.of(Instant.ofEpochMilli(1_750_000_000_000L)),
Optional.of(Instant.ofEpochMilli(1_750_000_100_000L)),
Map.of(ownerId, Instant.ofEpochMilli(1_750_001_000_000L))
Map.of(ownerId, Instant.ofEpochMilli(1_750_001_000_000L)),
true
);
PersistentState expected = new PersistentState(Map.of(playerId, player));
YamlBaseStateRepository repository =
@@ -39,6 +40,24 @@ final class YamlBaseStateRepositoryTest {
assertEquals(expected, repository.load());
}
@Test
void olderStateWithoutBorderPreferenceDefaultsToDisabled() throws Exception {
UUID playerId = UUID.randomUUID();
Path stateFile = temporaryDirectory.resolve("state.yml");
Files.writeString(stateFile, """
players:
%s:
name: Alex
""".formatted(playerId));
PlayerState player = new YamlBaseStateRepository(stateFile)
.load()
.players()
.get(playerId);
assertFalse(player.borderEnabled());
}
@Test
void invalidRecordsCannotGrantProgression() throws Exception {
UUID playerId = UUID.randomUUID();