feat(border): add base boundary particles
This commit is contained in:
@@ -31,6 +31,7 @@ The plugin JAR is written to `build/libs/`.
|
|||||||
/basesettings visitors <allowed|blocked>
|
/basesettings visitors <allowed|blocked>
|
||||||
/basesettings navigation <enable|disable>
|
/basesettings navigation <enable|disable>
|
||||||
/basesettings flight <enable|disable>
|
/basesettings flight <enable|disable>
|
||||||
|
/basesettings border <enable|disable>
|
||||||
/basesettings bossbar <enable|disable>
|
/basesettings bossbar <enable|disable>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -69,3 +69,10 @@ description: Chronological record of material decisions affecting the Spigot Bas
|
|||||||
- Removed the unsafe unconfirmed base reset path; complete resets now require `reset <player> all confirm`.
|
- Removed the unsafe unconfirmed base reset path; complete resets now require `reset <player> all confirm`.
|
||||||
- Tightened argument validation and replaced implementation-specific enum errors with player-facing usage guidance.
|
- Tightened argument validation and replaced implementation-specific enum errors with player-facing usage guidance.
|
||||||
- Verified the administrative revision with `./gradlew clean check jar`.
|
- Verified the administrative revision with `./gradlew clean check jar`.
|
||||||
|
|
||||||
|
## 2026-08-10 — Base border visualization
|
||||||
|
|
||||||
|
- Added persisted, owner-only base border particles controlled by `/basesettings border enable|disable` with contextual autocomplete.
|
||||||
|
- Rendered a bounded nearby arc at the player's current height using the current base radius while respecting world and vertical bounds.
|
||||||
|
- Added border preference reporting to player and administrative status output and safe defaults for existing state files.
|
||||||
|
- Verified the feature with `./gradlew clean check jar`.
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ description: Catalog of user stories for the Spigot Base plugin.
|
|||||||
10. [US-010: Administer player progression](us-010-administer-player-progression.md)
|
10. [US-010: Administer player progression](us-010-administer-player-progression.md)
|
||||||
11. [US-011: Configure and persist progression](us-011-configure-and-persist-progression.md)
|
11. [US-011: Configure and persist progression](us-011-configure-and-persist-progression.md)
|
||||||
12. [US-012: Build and release the plugin](us-012-build-and-release-plugin.md)
|
12. [US-012: Build and release the plugin](us-012-build-and-release-plugin.md)
|
||||||
|
13. [US-013: Visualize the base border](us-013-visualize-base-border.md)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
type: User Story
|
||||||
|
title: "US-013: Visualize the base border"
|
||||||
|
description: Let players display a bounded, owner-only particle arc along the current edge of their established base.
|
||||||
|
status: done
|
||||||
|
---
|
||||||
|
|
||||||
|
# US-013: Visualize the base border
|
||||||
|
|
||||||
|
As a **player with Base I**, I want to display a particle effect at my base border so that I can understand where base benefits begin and end while building.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] Border visualization requires Base I and an established base.
|
||||||
|
- [x] `/basesettings border enable` enables visualization idempotently, and `/basesettings border disable` disables it idempotently.
|
||||||
|
- [x] `border`, `enable`, and `disable` are offered through contextual autocomplete.
|
||||||
|
- [x] Border visualization is disabled by default and the preference persists across reconnects and restarts.
|
||||||
|
- [x] Older state files without the preference load it as disabled.
|
||||||
|
- [x] `/basesettings status` and `/baseadmin status` display the border preference.
|
||||||
|
- [x] Particles are visible only to the base owner.
|
||||||
|
- [x] Particles trace the current circular horizontal boundary at the player's current Y-level and follow base expansion or relocation immediately.
|
||||||
|
- [x] Particles render only while the player is in the base world, within the base's vertical range, and reasonably close to the boundary.
|
||||||
|
- [x] Particle generation is bounded to avoid excessive server and client load.
|
||||||
|
- [x] A complete administrative reset disables border visualization.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [US-001: Unlock and establish Base I](us-001-unlock-and-establish-base.md)
|
||||||
|
- [US-003: Expand the base](us-003-expand-the-base.md)
|
||||||
|
- [US-009: View progression and unlock notifications](us-009-view-progression-and-notifications.md)
|
||||||
|
- [US-010: Administer player progression](us-010-administer-player-progression.md)
|
||||||
@@ -97,8 +97,8 @@ final class BaseAdminCommand implements CommandExecutor, TabCompleter {
|
|||||||
sender.sendMessage(ChatColor.GRAY + "In-base placements=" + player.blocksPlacedInBase()
|
sender.sendMessage(ChatColor.GRAY + "In-base placements=" + player.blocksPlacedInBase()
|
||||||
+ " breaks=" + player.blocksBrokenInBase());
|
+ " breaks=" + player.blocksBrokenInBase());
|
||||||
sender.sendMessage(ChatColor.GRAY + "Toggles: navigation=" + player.navigationEnabled()
|
sender.sendMessage(ChatColor.GRAY + "Toggles: navigation=" + player.navigationEnabled()
|
||||||
+ " flight=" + player.flightEnabled() + " bossbar=" + player.bossBarEnabled()
|
+ " flight=" + player.flightEnabled() + " border=" + player.borderEnabled()
|
||||||
+ " visitors=" + player.visitorsEnabled());
|
+ " bossbar=" + player.bossBarEnabled() + " visitors=" + player.visitorsEnabled());
|
||||||
sender.sendMessage(ChatColor.GRAY + "Base: " + player.base()
|
sender.sendMessage(ChatColor.GRAY + "Base: " + player.base()
|
||||||
.map(base -> base.worldName() + " " + base.x() + "," + base.y() + "," + base.z())
|
.map(base -> base.worldName() + " " + base.x() + "," + base.y() + "," + base.z())
|
||||||
.orElse("not set"));
|
.orElse("not set"));
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package games.dmg.spigotbase;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Particle;
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
final class BaseBorderController implements Runnable {
|
||||||
|
private final Server server;
|
||||||
|
private final BaseStateManager stateManager;
|
||||||
|
private final BaseBoundsService boundsService;
|
||||||
|
|
||||||
|
BaseBorderController(
|
||||||
|
Server server,
|
||||||
|
BaseStateManager stateManager,
|
||||||
|
BaseBoundsService boundsService
|
||||||
|
) {
|
||||||
|
this.server = server;
|
||||||
|
this.stateManager = stateManager;
|
||||||
|
this.boundsService = boundsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
for (Player player : server.getOnlinePlayers()) {
|
||||||
|
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
|
||||||
|
if (!state.borderEnabled() || state.base().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BaseLocation base = state.base().orElseThrow();
|
||||||
|
if (!player.getWorld().getUID().equals(base.worldId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Location playerLocation = player.getLocation();
|
||||||
|
long minimumY = (long) base.y() - boundsService.verticalRange(state);
|
||||||
|
long maximumY = (long) base.y() + boundsService.verticalRange(state);
|
||||||
|
if (playerLocation.getY() < minimumY || playerLocation.getY() > maximumY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (BaseBorderGeometry.Point point : BaseBorderGeometry.visiblePoints(
|
||||||
|
base.x() + 0.5,
|
||||||
|
base.z() + 0.5,
|
||||||
|
boundsService.radius(state),
|
||||||
|
playerLocation.getX(),
|
||||||
|
playerLocation.getZ()
|
||||||
|
)) {
|
||||||
|
Location particle = new Location(
|
||||||
|
player.getWorld(),
|
||||||
|
point.x(),
|
||||||
|
playerLocation.getY() + 0.15,
|
||||||
|
point.z()
|
||||||
|
);
|
||||||
|
player.spawnParticle(
|
||||||
|
Particle.END_ROD,
|
||||||
|
particle,
|
||||||
|
1,
|
||||||
|
0.0, 0.0, 0.0, 0.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package games.dmg.spigotbase;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class BaseBorderGeometry {
|
||||||
|
static final int MAX_PARTICLES = 64;
|
||||||
|
static final double VISIBILITY_DISTANCE = 32.0;
|
||||||
|
private static final double PARTICLE_SPACING = 1.0;
|
||||||
|
|
||||||
|
private BaseBorderGeometry() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Point> visiblePoints(
|
||||||
|
double centerX,
|
||||||
|
double centerZ,
|
||||||
|
int radius,
|
||||||
|
double playerX,
|
||||||
|
double playerZ
|
||||||
|
) {
|
||||||
|
if (radius <= 0) {
|
||||||
|
throw new IllegalArgumentException("border radius must be positive");
|
||||||
|
}
|
||||||
|
double playerRadius = Math.hypot(playerX - centerX, playerZ - centerZ);
|
||||||
|
if (Math.abs(playerRadius - radius) > VISIBILITY_DISTANCE) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
double circumference = 2.0 * Math.PI * radius;
|
||||||
|
int count = Math.min(MAX_PARTICLES, Math.max(1, (int) Math.ceil(
|
||||||
|
circumference / PARTICLE_SPACING
|
||||||
|
)));
|
||||||
|
double centerAngle = Math.atan2(playerZ - centerZ, playerX - centerX);
|
||||||
|
double angleStep = circumference <= MAX_PARTICLES * PARTICLE_SPACING
|
||||||
|
? 2.0 * Math.PI / count
|
||||||
|
: PARTICLE_SPACING / radius;
|
||||||
|
double startAngle = circumference <= MAX_PARTICLES * PARTICLE_SPACING
|
||||||
|
? 0.0
|
||||||
|
: centerAngle - angleStep * (count - 1) / 2.0;
|
||||||
|
|
||||||
|
List<Point> points = new ArrayList<>(count);
|
||||||
|
double visibilitySquared = VISIBILITY_DISTANCE * VISIBILITY_DISTANCE;
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
double angle = startAngle + angleStep * index;
|
||||||
|
double x = centerX + radius * Math.cos(angle);
|
||||||
|
double z = centerZ + radius * Math.sin(angle);
|
||||||
|
double deltaX = x - playerX;
|
||||||
|
double deltaZ = z - playerZ;
|
||||||
|
if (deltaX * deltaX + deltaZ * deltaZ <= visibilitySquared) {
|
||||||
|
points.add(new Point(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Point(double x, double z) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import org.bukkit.inventory.PlayerInventory;
|
|||||||
|
|
||||||
final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||||
private static final List<String> SETTINGS = List.of(
|
private static final List<String> SETTINGS = List.of(
|
||||||
"status", "upgrade", "visitors", "navigation", "flight", "bossbar"
|
"status", "upgrade", "visitors", "navigation", "flight", "border", "bossbar"
|
||||||
);
|
);
|
||||||
private static final List<String> VISITOR_MODES = List.of("allowed", "blocked");
|
private static final List<String> VISITOR_MODES = List.of("allowed", "blocked");
|
||||||
private static final List<String> ENABLE_MODES = List.of("enable", "disable");
|
private static final List<String> ENABLE_MODES = List.of("enable", "disable");
|
||||||
@@ -59,6 +59,9 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("flight")) {
|
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("flight")) {
|
||||||
return updateFlight(player, state, arguments[1]);
|
return updateFlight(player, state, arguments[1]);
|
||||||
}
|
}
|
||||||
|
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("border")) {
|
||||||
|
return updateBorder(player, state, arguments[1]);
|
||||||
|
}
|
||||||
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("bossbar")) {
|
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("bossbar")) {
|
||||||
return updateBossBar(player, arguments[1]);
|
return updateBossBar(player, arguments[1]);
|
||||||
}
|
}
|
||||||
@@ -189,6 +192,29 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean updateBorder(Player player, PlayerState state, String mode) {
|
||||||
|
if (state.baseLevel() < 1 || state.base().isEmpty()) {
|
||||||
|
player.sendMessage(ChatColor.RED
|
||||||
|
+ "Establish Base I before enabling border visualization.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Boolean enabled = enabledMode(mode);
|
||||||
|
if (enabled == null) {
|
||||||
|
sendUsage(player);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
state = stateManager.update(
|
||||||
|
player.getUniqueId(),
|
||||||
|
player.getName(),
|
||||||
|
current -> current.withBorderEnabled(enabled)
|
||||||
|
);
|
||||||
|
stateManager.saveIfDirty();
|
||||||
|
player.sendMessage(ChatColor.YELLOW + "Base border visualization is now "
|
||||||
|
+ (state.borderEnabled() ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled")
|
||||||
|
+ ChatColor.YELLOW + ".");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean updateBossBar(Player player, String mode) {
|
private boolean updateBossBar(Player player, String mode) {
|
||||||
Boolean enabled = enabledMode(mode);
|
Boolean enabled = enabledMode(mode);
|
||||||
if (enabled == null) {
|
if (enabled == null) {
|
||||||
@@ -218,6 +244,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
+ (state.visitorsEnabled() ? "allowed" : "blocked")
|
+ (state.visitorsEnabled() ? "allowed" : "blocked")
|
||||||
+ " navigation=" + (state.navigationEnabled() ? "enabled" : "disabled")
|
+ " navigation=" + (state.navigationEnabled() ? "enabled" : "disabled")
|
||||||
+ " flight=" + (state.flightEnabled() ? "enabled" : "disabled")
|
+ " flight=" + (state.flightEnabled() ? "enabled" : "disabled")
|
||||||
|
+ " border=" + (state.borderEnabled() ? "enabled" : "disabled")
|
||||||
+ " bossbar=" + (state.bossBarEnabled() ? "enabled" : "disabled"));
|
+ " bossbar=" + (state.bossBarEnabled() ? "enabled" : "disabled"));
|
||||||
state.base().ifPresentOrElse(
|
state.base().ifPresentOrElse(
|
||||||
base -> player.sendMessage(ChatColor.GRAY + "Base: " + base.worldName() + " "
|
base -> player.sendMessage(ChatColor.GRAY + "Base: " + base.worldName() + " "
|
||||||
@@ -306,7 +333,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
List<String> modes = arguments[0].equalsIgnoreCase("visitors")
|
List<String> modes = arguments[0].equalsIgnoreCase("visitors")
|
||||||
? VISITOR_MODES
|
? VISITOR_MODES
|
||||||
: switch (arguments[0].toLowerCase(Locale.ROOT)) {
|
: switch (arguments[0].toLowerCase(Locale.ROOT)) {
|
||||||
case "navigation", "flight", "bossbar" -> ENABLE_MODES;
|
case "navigation", "flight", "border", "bossbar" -> ENABLE_MODES;
|
||||||
default -> List.of();
|
default -> List.of();
|
||||||
};
|
};
|
||||||
String prefix = arguments[1].toLowerCase(Locale.ROOT);
|
String prefix = arguments[1].toLowerCase(Locale.ROOT);
|
||||||
@@ -372,6 +399,6 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
private static void sendUsage(Player player) {
|
private static void sendUsage(Player player) {
|
||||||
player.sendMessage(ChatColor.RED + "Usage: /basesettings "
|
player.sendMessage(ChatColor.RED + "Usage: /basesettings "
|
||||||
+ "[status|upgrade|visitors <allowed|blocked>|navigation <enable|disable>"
|
+ "[status|upgrade|visitors <allowed|blocked>|navigation <enable|disable>"
|
||||||
+ "|flight <enable|disable>|bossbar <enable|disable>]");
|
+ "|flight <enable|disable>|border <enable|disable>|bossbar <enable|disable>]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ public record PlayerState(
|
|||||||
boolean visitorsEnabled,
|
boolean visitorsEnabled,
|
||||||
Optional<Instant> lastBaseSet,
|
Optional<Instant> lastBaseSet,
|
||||||
Optional<Instant> lastBaseTeleport,
|
Optional<Instant> lastBaseTeleport,
|
||||||
Map<UUID, Instant> visitorCooldownUntil
|
Map<UUID, Instant> visitorCooldownUntil,
|
||||||
|
boolean borderEnabled
|
||||||
) {
|
) {
|
||||||
public PlayerState {
|
public PlayerState {
|
||||||
if (playerId == null) {
|
if (playerId == null) {
|
||||||
@@ -69,12 +70,47 @@ public record PlayerState(
|
|||||||
if (baseLevel < 4 && visitorsEnabled) {
|
if (baseLevel < 4 && visitorsEnabled) {
|
||||||
throw new IllegalArgumentException("visitor access requires Base IV");
|
throw new IllegalArgumentException("visitor access requires Base IV");
|
||||||
}
|
}
|
||||||
|
if (borderEnabled && (baseLevel < 1 || base.isEmpty())) {
|
||||||
|
throw new IllegalArgumentException("border visualization requires an established Base I");
|
||||||
|
}
|
||||||
if (visitorCooldownUntil.entrySet().stream().anyMatch(entry ->
|
if (visitorCooldownUntil.entrySet().stream().anyMatch(entry ->
|
||||||
entry.getKey() == null || entry.getValue() == null)) {
|
entry.getKey() == null || entry.getValue() == null)) {
|
||||||
throw new IllegalArgumentException("visitor cooldowns must be complete");
|
throw new IllegalArgumentException("visitor cooldowns must be complete");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PlayerState(
|
||||||
|
UUID playerId,
|
||||||
|
String latestName,
|
||||||
|
Optional<BaseLocation> base,
|
||||||
|
int baseLevel,
|
||||||
|
int sizeLevel,
|
||||||
|
int flightLevel,
|
||||||
|
int warmupLevel,
|
||||||
|
int cooldownLevel,
|
||||||
|
long grassAndDirtBroken,
|
||||||
|
long stoneBroken,
|
||||||
|
long deepslateBroken,
|
||||||
|
long obsidianBroken,
|
||||||
|
long blocksPlacedInBase,
|
||||||
|
long blocksBrokenInBase,
|
||||||
|
boolean navigationEnabled,
|
||||||
|
boolean flightEnabled,
|
||||||
|
boolean bossBarEnabled,
|
||||||
|
boolean visitorsEnabled,
|
||||||
|
Optional<Instant> lastBaseSet,
|
||||||
|
Optional<Instant> lastBaseTeleport,
|
||||||
|
Map<UUID, Instant> visitorCooldownUntil
|
||||||
|
) {
|
||||||
|
this(
|
||||||
|
playerId, latestName, base, baseLevel, sizeLevel, flightLevel,
|
||||||
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static PlayerState newPlayer(UUID playerId, String latestName) {
|
public static PlayerState newPlayer(UUID playerId, String latestName) {
|
||||||
return new PlayerState(
|
return new PlayerState(
|
||||||
playerId,
|
playerId,
|
||||||
@@ -93,7 +129,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +139,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, count, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, count, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +149,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, enabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, enabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +159,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
enabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
enabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +169,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stone, deepslate,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stone, deepslate,
|
||||||
obsidian, blocksPlacedInBase, blocksBrokenInBase,
|
obsidian, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +179,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, enabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, enabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +199,7 @@ public record PlayerState(
|
|||||||
newWarmupLevel, newCooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
newWarmupLevel, newCooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, placements, breaks,
|
obsidianBroken, placements, breaks,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +215,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, Optional.of(usedAt), visitorCooldownUntil
|
lastBaseSet, Optional.of(usedAt), visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +225,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +242,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassDirt, stone, deepslate,
|
warmupLevel, cooldownLevel, grassDirt, stone, deepslate,
|
||||||
obsidian, placements, baseBreaks,
|
obsidian, placements, baseBreaks,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +261,8 @@ public record PlayerState(
|
|||||||
newWarmupLevel, newCooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
newWarmupLevel, newCooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
newNavigationEnabled, newFlightEnabled, bossBarEnabled, newVisitorsEnabled,
|
newNavigationEnabled, newFlightEnabled, bossBarEnabled, newVisitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil,
|
||||||
|
borderEnabled && newBaseLevel >= 1 && base.isPresent()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +272,17 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, enabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, enabled,
|
||||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerState withBorderEnabled(boolean enabled) {
|
||||||
|
return new PlayerState(
|
||||||
|
playerId, latestName, base, baseLevel, sizeLevel, flightLevel,
|
||||||
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
|
lastBaseSet, lastBaseTeleport, visitorCooldownUntil, enabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +294,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, cooldowns
|
lastBaseSet, lastBaseTeleport, cooldowns, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +304,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, Optional.empty(), visitorCooldownUntil
|
lastBaseSet, Optional.empty(), visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +314,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
lastBaseSet, lastBaseTeleport, Map.of()
|
lastBaseSet, lastBaseTeleport, Map.of(), borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +324,7 @@ public record PlayerState(
|
|||||||
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
warmupLevel, cooldownLevel, grassAndDirtBroken, stoneBroken, deepslateBroken,
|
||||||
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
obsidianBroken, blocksPlacedInBase, blocksBrokenInBase,
|
||||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||||
Optional.of(setAt), lastBaseTeleport, visitorCooldownUntil
|
Optional.of(setAt), lastBaseTeleport, visitorCooldownUntil, borderEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
|||||||
10L,
|
10L,
|
||||||
10L
|
10L
|
||||||
);
|
);
|
||||||
|
getServer().getScheduler().runTaskTimer(
|
||||||
|
this,
|
||||||
|
new BaseBorderController(getServer(), stateManager, boundsService),
|
||||||
|
10L,
|
||||||
|
10L
|
||||||
|
);
|
||||||
getServer().getScheduler().runTaskTimer(this, flightController, 5L, 5L);
|
getServer().getScheduler().runTaskTimer(this, flightController, 5L, 5L);
|
||||||
getServer().getScheduler().runTaskTimer(this, stateManager::saveIfDirty, 600L, 600L);
|
getServer().getScheduler().runTaskTimer(this, stateManager::saveIfDirty, 600L, 600L);
|
||||||
getLogger().info("Spigot Base enabled.");
|
getLogger().info("Spigot Base enabled.");
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ public final class YamlBaseStateRepository {
|
|||||||
yaml.getBoolean(path + ".visitors-enabled", false),
|
yaml.getBoolean(path + ".visitors-enabled", false),
|
||||||
instant(yaml, path + ".last-base-set-epoch-millis"),
|
instant(yaml, path + ".last-base-set-epoch-millis"),
|
||||||
instant(yaml, path + ".last-base-teleport-epoch-millis"),
|
instant(yaml, path + ".last-base-teleport-epoch-millis"),
|
||||||
loadVisitorCooldowns(yaml, path + ".visitor-cooldowns")
|
loadVisitorCooldowns(yaml, path + ".visitor-cooldowns"),
|
||||||
|
yaml.getBoolean(path + ".border-enabled", false)
|
||||||
);
|
);
|
||||||
players.put(playerId, player);
|
players.put(playerId, player);
|
||||||
} catch (IllegalArgumentException ignored) {
|
} catch (IllegalArgumentException ignored) {
|
||||||
@@ -222,6 +223,7 @@ public final class YamlBaseStateRepository {
|
|||||||
yaml.set(path + ".flight-enabled", player.flightEnabled());
|
yaml.set(path + ".flight-enabled", player.flightEnabled());
|
||||||
yaml.set(path + ".boss-bar-enabled", player.bossBarEnabled());
|
yaml.set(path + ".boss-bar-enabled", player.bossBarEnabled());
|
||||||
yaml.set(path + ".visitors-enabled", player.visitorsEnabled());
|
yaml.set(path + ".visitors-enabled", player.visitorsEnabled());
|
||||||
|
yaml.set(path + ".border-enabled", player.borderEnabled());
|
||||||
yaml.set(
|
yaml.set(
|
||||||
path + ".last-base-set-epoch-millis",
|
path + ".last-base-set-epoch-millis",
|
||||||
player.lastBaseSet().map(Instant::toEpochMilli).orElse(null)
|
player.lastBaseSet().map(Instant::toEpochMilli).orElse(null)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ commands:
|
|||||||
aliases: [visit]
|
aliases: [visit]
|
||||||
basesettings:
|
basesettings:
|
||||||
description: View progression, upgrade, and manage base settings.
|
description: View progression, upgrade, and manage base settings.
|
||||||
usage: /basesettings [status|upgrade|visitors|navigation|flight|bossbar]
|
usage: /basesettings [status|upgrade|visitors|navigation|flight|border|bossbar]
|
||||||
aliases: [homesettings]
|
aliases: [homesettings]
|
||||||
baseadmin:
|
baseadmin:
|
||||||
description: Administer Spigot Base.
|
description: Administer Spigot Base.
|
||||||
|
|||||||
@@ -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.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -36,6 +37,22 @@ final class AdminProgressionServiceTest {
|
|||||||
assertFalse(updated.visitorsEnabled());
|
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
|
@Test
|
||||||
void rejectsUnknownLevel() {
|
void rejectsUnknownLevel() {
|
||||||
PlayerState player = PlayerState.newPlayer(UUID.randomUUID(), "Alex");
|
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;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
final class BaseSettingsCommandTest {
|
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
|
@Test
|
||||||
void invalidArgumentsShowUsage() {
|
void invalidArgumentsShowUsage() {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
@@ -58,7 +73,7 @@ final class BaseSettingsCommandTest {
|
|||||||
List.of("allowed"),
|
List.of("allowed"),
|
||||||
command.onTabComplete(null, null, "basesettings", new String[] {"visitors", "a"})
|
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(
|
assertEquals(
|
||||||
List.of("disable"),
|
List.of("disable"),
|
||||||
command.onTabComplete(null, null, "basesettings", new String[] {setting, "d"})
|
command.onTabComplete(null, null, "basesettings", new String[] {setting, "d"})
|
||||||
@@ -75,7 +90,7 @@ final class BaseSettingsCommandTest {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assertEquals(
|
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[] {""})
|
command.onTabComplete(null, null, "basesettings", new String[] {""})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -194,6 +209,7 @@ final class BaseSettingsCommandTest {
|
|||||||
message.contains("Settings: visitors=")
|
message.contains("Settings: visitors=")
|
||||||
&& message.contains("navigation=")
|
&& message.contains("navigation=")
|
||||||
&& message.contains("flight=")
|
&& message.contains("flight=")
|
||||||
|
&& message.contains("border=")
|
||||||
&& message.contains("bossbar=")
|
&& message.contains("bossbar=")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,31 @@
|
|||||||
package games.dmg.spigotbase;
|
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.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
final class PlayerStateTest {
|
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
|
@Test
|
||||||
void rejectsLevelsOutsideKnownRanges() {
|
void rejectsLevelsOutsideKnownRanges() {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ final class YamlBaseStateRepositoryTest {
|
|||||||
true, true, false, true,
|
true, true, false, true,
|
||||||
Optional.of(Instant.ofEpochMilli(1_750_000_000_000L)),
|
Optional.of(Instant.ofEpochMilli(1_750_000_000_000L)),
|
||||||
Optional.of(Instant.ofEpochMilli(1_750_000_100_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));
|
PersistentState expected = new PersistentState(Map.of(playerId, player));
|
||||||
YamlBaseStateRepository repository =
|
YamlBaseStateRepository repository =
|
||||||
@@ -39,6 +40,24 @@ final class YamlBaseStateRepositoryTest {
|
|||||||
assertEquals(expected, repository.load());
|
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
|
@Test
|
||||||
void invalidRecordsCannotGrantProgression() throws Exception {
|
void invalidRecordsCannotGrantProgression() throws Exception {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
|
|||||||
Reference in New Issue
Block a user