feat(admin): complete configurable progression
Release / release (push) Successful in 3m57s
CI / build (push) Successful in 54s

This commit is contained in:
dmg
2026-08-09 15:12:04 -04:00
parent c87d923cad
commit 8b1064b971
41 changed files with 924 additions and 626 deletions
@@ -1,6 +1,16 @@
package games.dmg.spigotbase;
public final class AdminProgressionService {
private final PluginSettingsProvider settings;
public AdminProgressionService() {
this(new PluginSettingsProvider(PluginSettings.from(java.util.Map.of())));
}
public AdminProgressionService(PluginSettingsProvider settings) {
this.settings = settings;
}
public PlayerState setLevel(PlayerState player, ProgressionPath path, int level) {
int base = player.baseLevel();
int size = player.sizeLevel();
@@ -73,6 +83,118 @@ public final class AdminProgressionService {
);
}
public PlayerState setProgress(PlayerState player, ProgressCounter counter, long amount) {
if (amount < 0) {
throw new IllegalArgumentException("progress amount must not be negative");
}
long grassDirt = player.grassAndDirtBroken();
long stone = player.stoneBroken();
long deepslate = player.deepslateBroken();
long obsidian = player.obsidianBroken();
long placements = player.blocksPlacedInBase();
long baseBreaks = player.blocksBrokenInBase();
switch (counter) {
case GRASS_DIRT -> grassDirt = amount;
case STONE -> stone = amount;
case DEEPSLATE -> deepslate = amount;
case OBSIDIAN -> obsidian = amount;
case PLACEMENTS -> placements = amount;
case BASE_BREAKS -> baseBreaks = amount;
}
PluginSettings configured = settings.current();
int base = player.baseLevel();
if (grassDirt >= configured.navigationUnlockBlocks()) {
base = Math.max(base, 2);
} else if (grassDirt >= configured.baseUnlockBlocks()) {
base = Math.max(base, 1);
}
int size = player.sizeLevel();
if (stone >= configured.stoneExpansionBlocks()) {
base = Math.max(base, 1);
size = Math.max(size, 1);
}
if (deepslate >= configured.deepslateExpansionBlocks()) {
base = Math.max(base, 1);
size = Math.max(size, 2);
}
if (obsidian >= configured.obsidianExpansionBlocks()) {
base = Math.max(base, 1);
size = Math.max(size, 3);
}
int warmup = player.warmupLevel();
if (placements >= configured.teleportUnlockPlacements()) {
base = Math.max(base, 3);
}
if (placements >= configured.instantWarmupPlacements()) {
warmup = Math.max(warmup, 3);
} else if (placements >= configured.thirdWarmupPlacements()) {
warmup = Math.max(warmup, 2);
} else if (placements >= configured.secondWarmupPlacements()) {
warmup = Math.max(warmup, 1);
}
int cooldown = player.cooldownLevel();
if (baseBreaks >= configured.firstCooldownBreaks()) {
base = Math.max(base, 3);
}
if (baseBreaks >= configured.instantCooldownBreaks()) {
cooldown = Math.max(cooldown, 4);
} else if (baseBreaks >= configured.thirdCooldownBreaks()) {
cooldown = Math.max(cooldown, 3);
} else if (baseBreaks >= configured.secondCooldownBreaks()) {
cooldown = Math.max(cooldown, 2);
} else if (baseBreaks >= configured.firstCooldownBreaks()) {
cooldown = Math.max(cooldown, 1);
}
PlayerState updated = player.withProgressCounters(
grassDirt, stone, deepslate, obsidian, placements, baseBreaks
);
return updated.withAdministrativeLevels(
base,
size,
updated.flightLevel(),
warmup,
cooldown,
updated.navigationEnabled(),
updated.flightEnabled(),
updated.visitorsEnabled()
);
}
public PlayerState resetPath(PlayerState player, ProgressionPath path) {
if (path == ProgressionPath.BASE) {
return PlayerState.newPlayer(player.playerId(), player.latestName());
}
PlayerState cleared = switch (path) {
case SIZE -> player.withProgressCounters(
player.grassAndDirtBroken(),
0,
0,
0,
player.blocksPlacedInBase(),
player.blocksBrokenInBase()
);
case WARMUP -> player.withProgressCounters(
player.grassAndDirtBroken(),
player.stoneBroken(),
player.deepslateBroken(),
player.obsidianBroken(),
player.baseLevel() >= 3 ? settings.current().teleportUnlockPlacements() : 0,
player.blocksBrokenInBase()
);
case COOLDOWN -> player.withProgressCounters(
player.grassAndDirtBroken(),
player.stoneBroken(),
player.deepslateBroken(),
player.obsidianBroken(),
player.blocksPlacedInBase(),
0
);
case FLIGHT -> player;
case BASE -> throw new IllegalStateException("base reset already handled");
};
return setLevel(cleared, path, 0);
}
private static void requireRange(int value, int minimum, int maximum, String path) {
if (value < minimum || value > maximum) {
throw new IllegalArgumentException(
@@ -9,14 +9,24 @@ import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
final class BaseAdminCommand implements CommandExecutor {
private final JavaPlugin plugin;
private final BaseStateManager stateManager;
private final AdminProgressionService progressionService;
private final PluginSettingsProvider settingsProvider;
BaseAdminCommand(BaseStateManager stateManager, AdminProgressionService progressionService) {
BaseAdminCommand(
JavaPlugin plugin,
BaseStateManager stateManager,
AdminProgressionService progressionService,
PluginSettingsProvider settingsProvider
) {
this.plugin = plugin;
this.stateManager = stateManager;
this.progressionService = progressionService;
this.settingsProvider = settingsProvider;
}
@Override
@@ -25,6 +35,9 @@ final class BaseAdminCommand implements CommandExecutor {
sender.sendMessage(ChatColor.RED + "You do not have permission to administer Spigot Base.");
return true;
}
if (arguments.length > 0 && arguments[0].equalsIgnoreCase("config")) {
return updateConfig(sender, arguments);
}
if (arguments.length < 2) {
sendUsage(sender);
return true;
@@ -37,6 +50,7 @@ final class BaseAdminCommand implements CommandExecutor {
return switch (arguments[0].toLowerCase(Locale.ROOT)) {
case "progress" -> showProgress(sender, target.orElseThrow());
case "setlevel" -> setLevel(sender, target.orElseThrow(), arguments);
case "setprogress" -> setProgress(sender, target.orElseThrow(), arguments);
case "clearcooldown" -> clearCooldown(sender, target.orElseThrow(), arguments);
case "reset" -> reset(sender, target.orElseThrow(), arguments);
default -> {
@@ -92,6 +106,37 @@ final class BaseAdminCommand implements CommandExecutor {
return true;
}
private boolean setProgress(
CommandSender sender,
PlayerState target,
String[] arguments
) {
if (arguments.length != 4) {
sender.sendMessage(ChatColor.RED
+ "Usage: /baseadmin setprogress <player> <counter> <amount>");
return true;
}
try {
ProgressCounter counter = ProgressCounter.valueOf(
arguments[2].toUpperCase(Locale.ROOT).replace('-', '_')
);
long amount = Long.parseLong(arguments[3]);
PlayerState updated = stateManager.update(
target.playerId(),
target.latestName(),
current -> progressionService.setProgress(current, counter, amount)
);
stateManager.saveIfDirty();
sender.sendMessage(ChatColor.GREEN + "Set " + updated.latestName() + "'s "
+ counter.name().toLowerCase(Locale.ROOT) + " progress to " + amount + ".");
} catch (NumberFormatException exception) {
sender.sendMessage(ChatColor.RED + "The progress amount must be an integer.");
} catch (IllegalArgumentException exception) {
sender.sendMessage(ChatColor.RED + exception.getMessage());
}
return true;
}
private boolean clearCooldown(
CommandSender sender,
PlayerState target,
@@ -146,7 +191,7 @@ final class BaseAdminCommand implements CommandExecutor {
stateManager.update(
target.playerId(),
target.latestName(),
current -> progressionService.setLevel(current, path, 0)
current -> progressionService.resetPath(current, path)
);
stateManager.saveIfDirty();
sender.sendMessage(ChatColor.GREEN + "Reset " + path.name().toLowerCase(Locale.ROOT)
@@ -157,6 +202,43 @@ final class BaseAdminCommand implements CommandExecutor {
return true;
}
private boolean updateConfig(CommandSender sender, String[] arguments) {
if (arguments.length != 3) {
sender.sendMessage(ChatColor.RED + "Usage: /baseadmin config <key> <integer>");
return true;
}
String key = arguments[1];
Object previous = plugin.getConfig().get(key);
if (!(previous instanceof Number)) {
sender.sendMessage(ChatColor.RED + "Unknown numeric setting: " + key);
return true;
}
try {
long value = Long.parseLong(arguments[2]);
java.util.Map<String, Object> proposed =
new java.util.HashMap<>(plugin.getConfig().getValues(false));
proposed.put(key, value);
PluginSettings updated = PluginSettingsValidator.validateMaterials(
PluginSettings.from(proposed)
);
plugin.getConfig().set(key, value);
try {
plugin.saveConfig();
} catch (RuntimeException exception) {
plugin.getConfig().set(key, previous);
throw exception;
}
settingsProvider.update(updated);
sender.sendMessage(ChatColor.GREEN + "Updated " + key + " to " + value
+ "; the change is active immediately.");
} catch (NumberFormatException exception) {
sender.sendMessage(ChatColor.RED + "The setting value must be an integer.");
} catch (IllegalArgumentException exception) {
sender.sendMessage(ChatColor.RED + exception.getMessage());
}
return true;
}
private Optional<PlayerState> resolve(String name) {
Player online = Bukkit.getPlayerExact(name);
if (online != null) {
@@ -167,6 +249,7 @@ final class BaseAdminCommand implements CommandExecutor {
private static void sendUsage(CommandSender sender) {
sender.sendMessage(ChatColor.YELLOW + "Usage: /baseadmin "
+ "<progress|setlevel|clearcooldown|reset> <player> ...");
+ "<progress|setlevel|setprogress|clearcooldown|reset> <player> ...");
sender.sendMessage(ChatColor.YELLOW + " /baseadmin config <key> <integer>");
}
}
@@ -1,26 +1,30 @@
package games.dmg.spigotbase;
public final class BaseBoundsService {
private final PluginSettings settings;
private final PluginSettingsProvider settings;
public BaseBoundsService(PluginSettings settings) {
this(new PluginSettingsProvider(settings));
}
public BaseBoundsService(PluginSettingsProvider settings) {
this.settings = settings;
}
public int radius(PlayerState player) {
return switch (player.sizeLevel()) {
case 0 -> settings.initialRadius();
case 1 -> settings.firstExpandedRadius();
case 2 -> settings.secondExpandedRadius();
case 3 -> settings.thirdExpandedRadius();
case 0 -> settings.current().initialRadius();
case 1 -> settings.current().firstExpandedRadius();
case 2 -> settings.current().secondExpandedRadius();
case 3 -> settings.current().thirdExpandedRadius();
default -> throw new IllegalArgumentException("unknown size level");
};
}
public int verticalRange(PlayerState player) {
return switch (player.flightLevel()) {
case 0, 1 -> settings.initialVerticalRange();
case 2 -> settings.secondFlightVerticalRange();
case 0, 1 -> settings.current().initialVerticalRange();
case 2 -> settings.current().secondFlightVerticalRange();
case 3 -> Integer.MAX_VALUE;
default -> throw new IllegalArgumentException("unknown flight level");
};
@@ -14,13 +14,13 @@ final class BaseCommand implements CommandExecutor {
private final BaseTeleportManager teleportManager;
private final BaseStateManager stateManager;
private final VisitorPolicy visitorPolicy;
private final PluginSettings settings;
private final PluginSettingsProvider settings;
BaseCommand(
BaseTeleportManager teleportManager,
BaseStateManager stateManager,
VisitorPolicy visitorPolicy,
PluginSettings settings
PluginSettingsProvider settings
) {
this.teleportManager = teleportManager;
this.stateManager = stateManager;
@@ -54,14 +54,16 @@ final class BaseCommand implements CommandExecutor {
: "You must unlock Base III before purchasing Base IV."));
return;
}
int price = settings.visitorUnlockDiamondCost();
int price = settings.current().visitorUnlockDiamondCost();
Material currency = Material.valueOf(settings.current().visitorCurrencyMaterial());
PlayerInventory inventory = player.getInventory();
if (countDiamonds(inventory) < price) {
player.sendMessage(ChatColor.RED + "Base IV costs " + price + " diamonds.");
if (countCurrency(inventory, currency) < price) {
player.sendMessage(ChatColor.RED + "Base IV costs " + price + " "
+ currency.name().toLowerCase(java.util.Locale.ROOT) + ".");
return;
}
ItemStack[] snapshot = cloneContents(inventory.getStorageContents());
removeDiamonds(inventory, price);
removeCurrency(inventory, currency, price);
try {
stateManager.updateAndSave(
player.getUniqueId(),
@@ -76,27 +78,34 @@ final class BaseCommand implements CommandExecutor {
player.sendTitle(
ChatColor.GOLD + "Base IV Unlocked",
ChatColor.YELLOW + "Visitors may now teleport to your base",
10, 70, 20
settings.current().titleFadeInTicks(),
settings.current().titleStayTicks(),
settings.current().titleFadeOutTicks()
);
player.sendMessage(ChatColor.GREEN + "Base IV unlocked for " + price + " diamonds.");
player.sendMessage(ChatColor.GREEN + "Base IV unlocked for " + price + " "
+ currency.name().toLowerCase(java.util.Locale.ROOT) + ".");
}
private static int countDiamonds(PlayerInventory inventory) {
private static int countCurrency(PlayerInventory inventory, Material currency) {
int count = 0;
for (ItemStack item : inventory.getStorageContents()) {
if (item != null && item.getType() == Material.DIAMOND) {
if (item != null && item.getType() == currency) {
count += item.getAmount();
}
}
return count;
}
private static void removeDiamonds(PlayerInventory inventory, int amount) {
private static void removeCurrency(
PlayerInventory inventory,
Material currency,
int amount
) {
ItemStack[] contents = inventory.getStorageContents();
int remaining = amount;
for (int index = 0; index < contents.length && remaining > 0; index++) {
ItemStack item = contents[index];
if (item == null || item.getType() != Material.DIAMOND) {
if (item == null || item.getType() != currency) {
continue;
}
int removed = Math.min(remaining, item.getAmount());
@@ -15,7 +15,7 @@ final class BaseFlightController implements Runnable {
private final BaseStateManager stateManager;
private final SecondaryProgressionService progressionService;
private final BaseBoundsService boundsService;
private final PluginSettings settings;
private final PluginSettingsProvider settings;
private final Set<UUID> grantedFlight = new HashSet<>();
private final Set<UUID> warned = new HashSet<>();
@@ -24,7 +24,7 @@ final class BaseFlightController implements Runnable {
BaseStateManager stateManager,
SecondaryProgressionService progressionService,
BaseBoundsService boundsService,
PluginSettings settings
PluginSettingsProvider settings
) {
this.server = server;
this.stateManager = stateManager;
@@ -81,7 +81,9 @@ final class BaseFlightController implements Runnable {
player.sendTitle(
ChatColor.GOLD + "Base Flight " + roman(updated.flightLevel()) + " Unlocked",
ChatColor.YELLOW + "Use /baseflight to toggle flight",
10, 70, 20
settings.current().titleFadeInTicks(),
settings.current().titleStayTicks(),
settings.current().titleFadeOutTicks()
);
return updated;
}
@@ -101,7 +103,7 @@ final class BaseFlightController implements Runnable {
double deltaZ = player.getLocation().getZ() - (base.z() + 0.5);
double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
int radius = boundsService.radius(state);
int bufferedRadius = radius + settings.flightWarningBuffer();
int bufferedRadius = radius + settings.current().flightWarningBuffer();
if (distanceSquared > (double) bufferedRadius * bufferedRadius) {
removeGrantedFlight(player);
return;
@@ -115,7 +117,9 @@ final class BaseFlightController implements Runnable {
player.sendTitle(
ChatColor.RED + "Leaving Your Base",
ChatColor.YELLOW + "Turn back before base flight ends",
0, 30, 10
0,
Math.min(30, settings.current().titleStayTicks()),
settings.current().titleFadeOutTicks()
);
}
} else {
@@ -9,10 +9,16 @@ import org.bukkit.util.Vector;
final class BaseNavigationController implements Runnable {
private final Server server;
private final BaseStateManager stateManager;
private final PluginSettingsProvider settings;
BaseNavigationController(Server server, BaseStateManager stateManager) {
BaseNavigationController(
Server server,
BaseStateManager stateManager,
PluginSettingsProvider settings
) {
this.server = server;
this.stateManager = stateManager;
this.settings = settings;
}
@Override
@@ -36,7 +42,7 @@ final class BaseNavigationController implements Runnable {
continue;
}
direction.normalize();
for (int step = 1; step <= 5; step++) {
for (int step = 1; step <= settings.current().navigationParticleCount(); step++) {
Location particle = origin.clone().add(direction.clone().multiply(step));
player.spawnParticle(Particle.END_ROD, particle, 1, 0.0, 0.0, 0.0, 0.0);
}
@@ -8,10 +8,10 @@ import org.bukkit.entity.Player;
final class BaseProgressCommand implements CommandExecutor {
private final BaseStateManager stateManager;
private final PluginSettings settings;
private final PluginSettingsProvider settings;
private final TeleportPolicy teleportPolicy;
BaseProgressCommand(BaseStateManager stateManager, PluginSettings settings) {
BaseProgressCommand(BaseStateManager stateManager, PluginSettingsProvider settings) {
this.stateManager = stateManager;
this.settings = settings;
this.teleportPolicy = new TeleportPolicy(settings);
@@ -53,13 +53,13 @@ final class BaseProgressCommand implements CommandExecutor {
private void showBasePath(Player player, PlayerState state) {
String detail = switch (state.baseLevel()) {
case 0 -> state.grassAndDirtBroken() + "/" + settings.baseUnlockBlocks()
case 0 -> state.grassAndDirtBroken() + "/" + settings.current().baseUnlockBlocks()
+ " grass or dirt → /setbase";
case 1 -> state.grassAndDirtBroken() + "/" + settings.navigationUnlockBlocks()
case 1 -> state.grassAndDirtBroken() + "/" + settings.current().navigationUnlockBlocks()
+ " grass or dirt → navigation";
case 2 -> state.blocksPlacedInBase() + "/" + settings.teleportUnlockPlacements()
case 2 -> state.blocksPlacedInBase() + "/" + settings.current().teleportUnlockPlacements()
+ " placements → /base";
case 3 -> settings.visitorUnlockDiamondCost() + " diamonds → visitor access";
case 3 -> settings.current().visitorUnlockDiamondCost() + " diamonds → visitor access";
case 4 -> "complete; visitor access unlocked";
default -> "invalid";
};
@@ -69,9 +69,9 @@ final class BaseProgressCommand implements CommandExecutor {
private void showSizePath(Player player, PlayerState state) {
String detail = switch (state.sizeLevel()) {
case 0 -> state.stoneBroken() + "/" + settings.stoneExpansionBlocks() + " stone";
case 1 -> state.deepslateBroken() + "/" + settings.deepslateExpansionBlocks() + " deepslate";
case 2 -> state.obsidianBroken() + "/" + settings.obsidianExpansionBlocks() + " obsidian";
case 0 -> state.stoneBroken() + "/" + settings.current().stoneExpansionBlocks() + " stone";
case 1 -> state.deepslateBroken() + "/" + settings.current().deepslateExpansionBlocks() + " deepslate";
case 2 -> state.obsidianBroken() + "/" + settings.current().obsidianExpansionBlocks() + " obsidian";
case 3 -> "complete; 150-block radius by default";
default -> "invalid";
};
@@ -90,9 +90,9 @@ final class BaseProgressCommand implements CommandExecutor {
private void showWarmupPath(Player player, PlayerState state) {
String detail = switch (state.warmupLevel()) {
case 0 -> state.blocksPlacedInBase() + "/" + settings.secondWarmupPlacements();
case 1 -> state.blocksPlacedInBase() + "/" + settings.thirdWarmupPlacements();
case 2 -> state.blocksPlacedInBase() + "/" + settings.instantWarmupPlacements();
case 0 -> state.blocksPlacedInBase() + "/" + settings.current().secondWarmupPlacements();
case 1 -> state.blocksPlacedInBase() + "/" + settings.current().thirdWarmupPlacements();
case 2 -> state.blocksPlacedInBase() + "/" + settings.current().instantWarmupPlacements();
case 3 -> "complete";
default -> "invalid";
};
@@ -103,10 +103,10 @@ final class BaseProgressCommand implements CommandExecutor {
private void showCooldownPath(Player player, PlayerState state) {
String detail = switch (state.cooldownLevel()) {
case 0 -> state.blocksBrokenInBase() + "/" + settings.firstCooldownBreaks();
case 1 -> state.blocksBrokenInBase() + "/" + settings.secondCooldownBreaks();
case 2 -> state.blocksBrokenInBase() + "/" + settings.thirdCooldownBreaks();
case 3 -> state.blocksBrokenInBase() + "/" + settings.instantCooldownBreaks();
case 0 -> state.blocksBrokenInBase() + "/" + settings.current().firstCooldownBreaks();
case 1 -> state.blocksBrokenInBase() + "/" + settings.current().secondCooldownBreaks();
case 2 -> state.blocksBrokenInBase() + "/" + settings.current().thirdCooldownBreaks();
case 3 -> state.blocksBrokenInBase() + "/" + settings.current().instantCooldownBreaks();
case 4 -> "complete";
default -> "invalid";
};
@@ -19,15 +19,13 @@ import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.plugin.Plugin;
final class BaseProgressListener implements Listener {
private static final long BOSS_BAR_TICKS = 60L;
private final Plugin plugin;
private final BaseStateManager stateManager;
private final BaseProgressionService baseProgressionService;
private final SecondaryProgressionService secondaryProgressionService;
private final TeleportProgressionService teleportProgressionService;
private final BaseBoundsService boundsService;
private final PluginSettings settings;
private final PluginSettingsProvider settings;
private final Map<UUID, BossBar> activeBossBars = new HashMap<>();
BaseProgressListener(
@@ -37,7 +35,7 @@ final class BaseProgressListener implements Listener {
SecondaryProgressionService secondaryProgressionService,
TeleportProgressionService teleportProgressionService,
BaseBoundsService boundsService,
PluginSettings settings
PluginSettingsProvider settings
) {
this.plugin = plugin;
this.stateManager = stateManager;
@@ -63,13 +61,15 @@ final class BaseProgressListener implements Listener {
event.getBlock().getY(),
event.getBlock().getZ()
);
if (!isProgressMaterial(material) && !(insideBase && before.baseLevel() >= 3)) {
boolean cooldownEligible = insideBase && before.baseLevel() >= 3
&& !settings.current().cooldownExcludedMaterials().contains(material.name());
if (!isProgressMaterial(material) && !cooldownEligible) {
return;
}
ProgressionUpdate[] updateHolder = new ProgressionUpdate[1];
PlayerState state = stateManager.update(player.getUniqueId(), player.getName(), current -> {
ProgressionUpdate materialUpdate = updateForMaterial(current, material);
ProgressionUpdate breakUpdate = insideBase
ProgressionUpdate breakUpdate = cooldownEligible
? teleportProgressionService.recordBreak(materialUpdate.player())
: ProgressionUpdate.unchanged(materialUpdate.player());
ProgressionUpdate combined = combine(materialUpdate, breakUpdate);
@@ -82,7 +82,7 @@ final class BaseProgressListener implements Listener {
stateManager.saveIfDirty();
}
if (state.bossBarEnabled()) {
if (insideBase && before.baseLevel() >= 3) {
if (cooldownEligible) {
showProgress(player, cooldownDisplay(state));
} else {
showProgress(player, progressDisplay(state, material));
@@ -127,13 +127,21 @@ final class BaseProgressListener implements Listener {
}
private ProgressionUpdate updateForMaterial(PlayerState player, Material material) {
return switch (material) {
case GRASS_BLOCK, DIRT -> baseProgressionService.recordGrassOrDirtBreak(player);
case STONE -> secondaryProgressionService.recordStoneBreak(player);
case DEEPSLATE -> secondaryProgressionService.recordDeepslateBreak(player);
case OBSIDIAN -> secondaryProgressionService.recordObsidianBreak(player);
default -> ProgressionUpdate.unchanged(player);
};
String name = material.name();
PluginSettings configured = settings.current();
if (configured.baseUnlockMaterials().contains(name)) {
return baseProgressionService.recordGrassOrDirtBreak(player);
}
if (configured.stoneExpansionMaterial().equals(name)) {
return secondaryProgressionService.recordStoneBreak(player);
}
if (configured.deepslateExpansionMaterial().equals(name)) {
return secondaryProgressionService.recordDeepslateBreak(player);
}
if (configured.obsidianExpansionMaterial().equals(name)) {
return secondaryProgressionService.recordObsidianBreak(player);
}
return ProgressionUpdate.unchanged(player);
}
private void announceUnlock(Player player, ProgressionUpdate update) {
@@ -146,37 +154,48 @@ final class BaseProgressListener implements Listener {
case 4 -> "Visitors can now travel to your base";
default -> "A new base benefit is available";
};
player.sendTitle(
sendUnlockTitle(
player,
ChatColor.GOLD + "Base " + roman(level) + " Unlocked",
ChatColor.YELLOW + subtitle,
10, 70, 20
ChatColor.YELLOW + subtitle
);
player.sendMessage(ChatColor.GREEN + "You unlocked Base " + roman(level) + "! " + subtitle);
}
if (update.unlockedSizeLevel()) {
int radius = boundsService.radius(update.player());
player.sendTitle(
sendUnlockTitle(
player,
ChatColor.GOLD + "Base Size Upgraded",
ChatColor.YELLOW + "Your base radius is now " + radius + " blocks",
10, 70, 20
ChatColor.YELLOW + "Your base radius is now " + radius + " blocks"
);
}
if (update.unlockedWarmupLevel()) {
player.sendTitle(
sendUnlockTitle(
player,
ChatColor.GOLD + "Teleport Warm-up Improved",
ChatColor.YELLOW + "Your /base warm-up is now shorter",
10, 70, 20
ChatColor.YELLOW + "Your /base warm-up is now shorter"
);
}
if (update.unlockedCooldownLevel()) {
player.sendTitle(
sendUnlockTitle(
player,
ChatColor.GOLD + "Teleport Cooldown Improved",
ChatColor.YELLOW + "You can use /base more often",
10, 70, 20
ChatColor.YELLOW + "You can use /base more often"
);
}
}
private void sendUnlockTitle(Player player, String title, String subtitle) {
PluginSettings configured = settings.current();
player.sendTitle(
title,
subtitle,
configured.titleFadeInTicks(),
configured.titleStayTicks(),
configured.titleFadeOutTicks()
);
}
private void showProgress(Player player, ProgressDisplay display) {
if (display == null) {
removeBossBar(player.getUniqueId());
@@ -195,46 +214,54 @@ final class BaseProgressListener implements Listener {
if (activeBossBars.remove(player.getUniqueId(), bossBar)) {
bossBar.removeAll();
}
}, BOSS_BAR_TICKS);
}, settings.current().bossBarDurationTicks());
}
private ProgressDisplay progressDisplay(PlayerState state, Material material) {
return switch (material) {
case GRASS_BLOCK, DIRT -> state.baseLevel() < 2
? new ProgressDisplay(
state.baseLevel() == 0 ? "Base I" : "Base II",
state.grassAndDirtBroken(),
state.baseLevel() == 0 ? settings.baseUnlockBlocks() : settings.navigationUnlockBlocks()
)
: null;
case STONE -> state.sizeLevel() == 0
? new ProgressDisplay("Base Size II", state.stoneBroken(), settings.stoneExpansionBlocks())
: null;
case DEEPSLATE -> state.sizeLevel() == 1
? new ProgressDisplay("Base Size III", state.deepslateBroken(), settings.deepslateExpansionBlocks())
: null;
case OBSIDIAN -> state.sizeLevel() == 2
? new ProgressDisplay("Base Size IV", state.obsidianBroken(), settings.obsidianExpansionBlocks())
: null;
default -> null;
};
String name = material.name();
PluginSettings configured = settings.current();
if (configured.baseUnlockMaterials().contains(name) && state.baseLevel() < 2) {
return new ProgressDisplay(
state.baseLevel() == 0 ? "Base I" : "Base II",
state.grassAndDirtBroken(),
state.baseLevel() == 0
? configured.baseUnlockBlocks()
: configured.navigationUnlockBlocks()
);
}
if (configured.stoneExpansionMaterial().equals(name) && state.sizeLevel() == 0) {
return new ProgressDisplay(
"Base Size II", state.stoneBroken(), configured.stoneExpansionBlocks()
);
}
if (configured.deepslateExpansionMaterial().equals(name) && state.sizeLevel() == 1) {
return new ProgressDisplay(
"Base Size III", state.deepslateBroken(), configured.deepslateExpansionBlocks()
);
}
if (configured.obsidianExpansionMaterial().equals(name) && state.sizeLevel() == 2) {
return new ProgressDisplay(
"Base Size IV", state.obsidianBroken(), configured.obsidianExpansionBlocks()
);
}
return null;
}
private ProgressDisplay warmupDisplay(PlayerState state) {
if (state.baseLevel() == 2) {
return new ProgressDisplay(
"Base III", state.blocksPlacedInBase(), settings.teleportUnlockPlacements()
"Base III", state.blocksPlacedInBase(), settings.current().teleportUnlockPlacements()
);
}
return switch (state.warmupLevel()) {
case 0 -> new ProgressDisplay(
"15s Warm-up", state.blocksPlacedInBase(), settings.secondWarmupPlacements()
"15s Warm-up", state.blocksPlacedInBase(), settings.current().secondWarmupPlacements()
);
case 1 -> new ProgressDisplay(
"5s Warm-up", state.blocksPlacedInBase(), settings.thirdWarmupPlacements()
"5s Warm-up", state.blocksPlacedInBase(), settings.current().thirdWarmupPlacements()
);
case 2 -> new ProgressDisplay(
"Instant Warm-up", state.blocksPlacedInBase(), settings.instantWarmupPlacements()
"Instant Warm-up", state.blocksPlacedInBase(), settings.current().instantWarmupPlacements()
);
default -> null;
};
@@ -243,16 +270,16 @@ final class BaseProgressListener implements Listener {
private ProgressDisplay cooldownDisplay(PlayerState state) {
return switch (state.cooldownLevel()) {
case 0 -> new ProgressDisplay(
"2h Cooldown", state.blocksBrokenInBase(), settings.firstCooldownBreaks()
"2h Cooldown", state.blocksBrokenInBase(), settings.current().firstCooldownBreaks()
);
case 1 -> new ProgressDisplay(
"1h Cooldown", state.blocksBrokenInBase(), settings.secondCooldownBreaks()
"1h Cooldown", state.blocksBrokenInBase(), settings.current().secondCooldownBreaks()
);
case 2 -> new ProgressDisplay(
"30m Cooldown", state.blocksBrokenInBase(), settings.thirdCooldownBreaks()
"30m Cooldown", state.blocksBrokenInBase(), settings.current().thirdCooldownBreaks()
);
case 3 -> new ProgressDisplay(
"Instant Cooldown", state.blocksBrokenInBase(), settings.instantCooldownBreaks()
"Instant Cooldown", state.blocksBrokenInBase(), settings.current().instantCooldownBreaks()
);
default -> null;
};
@@ -289,10 +316,13 @@ final class BaseProgressListener implements Listener {
}
}
private static boolean isProgressMaterial(Material material) {
return material == Material.GRASS_BLOCK || material == Material.DIRT
|| material == Material.STONE || material == Material.DEEPSLATE
|| material == Material.OBSIDIAN;
private boolean isProgressMaterial(Material material) {
String name = material.name();
PluginSettings configured = settings.current();
return configured.baseUnlockMaterials().contains(name)
|| configured.stoneExpansionMaterial().equals(name)
|| configured.deepslateExpansionMaterial().equals(name)
|| configured.obsidianExpansionMaterial().equals(name);
}
private static String roman(int level) {
@@ -1,9 +1,13 @@
package games.dmg.spigotbase;
public final class BaseProgressionService {
private final PluginSettings settings;
private final PluginSettingsProvider settings;
public BaseProgressionService(PluginSettings settings) {
this(new PluginSettingsProvider(settings));
}
public BaseProgressionService(PluginSettingsProvider settings) {
this.settings = settings;
}
@@ -13,10 +17,10 @@ public final class BaseProgressionService {
: player.grassAndDirtBroken() + 1;
int previousLevel = player.baseLevel();
int baseLevel = previousLevel;
if (previousLevel == 0 && count >= settings.baseUnlockBlocks()) {
if (previousLevel == 0 && count >= settings.current().baseUnlockBlocks()) {
baseLevel = 1;
}
if (previousLevel == 1 && count >= settings.navigationUnlockBlocks()) {
if (previousLevel == 1 && count >= settings.current().navigationUnlockBlocks()) {
baseLevel = 2;
}
boolean unlocked = baseLevel != previousLevel;
@@ -5,9 +5,13 @@ import java.time.Instant;
import java.util.Optional;
public final class BaseService {
private final PluginSettings settings;
private final PluginSettingsProvider settings;
public BaseService(PluginSettings settings) {
this(new PluginSettingsProvider(settings));
}
public BaseService(PluginSettingsProvider settings) {
this.settings = settings;
}
@@ -23,7 +27,7 @@ public final class BaseService {
return Optional.empty();
}
Instant availableAt = player.lastBaseSet().orElseThrow()
.plusSeconds(settings.relocationCooldownSeconds());
.plusSeconds(settings.current().relocationCooldownSeconds());
if (!now.isBefore(availableAt)) {
return Optional.empty();
}
@@ -193,6 +193,23 @@ public record PlayerState(
);
}
public PlayerState withProgressCounters(
long grassDirt,
long stone,
long deepslate,
long obsidian,
long placements,
long baseBreaks
) {
return new PlayerState(
playerId, latestName, base, baseLevel, sizeLevel, flightLevel,
warmupLevel, cooldownLevel, grassDirt, stone, deepslate,
obsidian, placements, baseBreaks,
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
);
}
public PlayerState withAdministrativeLevels(
int newBaseLevel,
int newSizeLevel,
@@ -1,7 +1,10 @@
package games.dmg.spigotbase;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
public record PluginSettings(
int baseUnlockBlocks,
@@ -32,7 +35,18 @@ public record PluginSettings(
long secondTeleportCooldownSeconds,
long thirdTeleportCooldownSeconds,
long fourthTeleportCooldownSeconds,
int visitorUnlockDiamondCost
int visitorUnlockDiamondCost,
Set<String> baseUnlockMaterials,
String stoneExpansionMaterial,
String deepslateExpansionMaterial,
String obsidianExpansionMaterial,
String visitorCurrencyMaterial,
Set<String> cooldownExcludedMaterials,
int bossBarDurationTicks,
int navigationParticleCount,
int titleFadeInTicks,
int titleStayTicks,
int titleFadeOutTicks
) {
private static final int DEFAULT_BASE_UNLOCK_BLOCKS = 250;
private static final int DEFAULT_NAVIGATION_UNLOCK_BLOCKS = 500;
@@ -63,6 +77,11 @@ public record PluginSettings(
private static final long DEFAULT_THIRD_TELEPORT_COOLDOWN_SECONDS = 3_600L;
private static final long DEFAULT_FOURTH_TELEPORT_COOLDOWN_SECONDS = 1_800L;
private static final int DEFAULT_VISITOR_UNLOCK_DIAMOND_COST = 128;
private static final int DEFAULT_BOSS_BAR_DURATION_TICKS = 60;
private static final int DEFAULT_NAVIGATION_PARTICLE_COUNT = 5;
private static final int DEFAULT_TITLE_FADE_IN_TICKS = 10;
private static final int DEFAULT_TITLE_STAY_TICKS = 70;
private static final int DEFAULT_TITLE_FADE_OUT_TICKS = 20;
public PluginSettings {
requirePositive(baseUnlockBlocks, "base-unlock-blocks");
@@ -118,6 +137,25 @@ public record PluginSettings(
throw new IllegalArgumentException("teleport cooldown durations must decrease");
}
requirePositive(visitorUnlockDiamondCost, "visitor-unlock-diamond-cost");
baseUnlockMaterials = normalizedSet(baseUnlockMaterials, "base-unlock-materials", false);
stoneExpansionMaterial = normalizedName(stoneExpansionMaterial, "stone-expansion-material");
deepslateExpansionMaterial = normalizedName(
deepslateExpansionMaterial, "deepslate-expansion-material"
);
obsidianExpansionMaterial = normalizedName(
obsidianExpansionMaterial, "obsidian-expansion-material"
);
visitorCurrencyMaterial = normalizedName(
visitorCurrencyMaterial, "visitor-currency-material"
);
cooldownExcludedMaterials = normalizedSet(
cooldownExcludedMaterials, "cooldown-excluded-materials", true
);
requirePositive(bossBarDurationTicks, "boss-bar-duration-ticks");
requirePositive(navigationParticleCount, "navigation-particle-count");
requireNonNegative(titleFadeInTicks, "title-fade-in-ticks");
requirePositive(titleStayTicks, "title-stay-ticks");
requireNonNegative(titleFadeOutTicks, "title-fade-out-ticks");
}
public static PluginSettings from(Map<String, ?> values) {
@@ -167,10 +205,57 @@ public record PluginSettings(
"fourth-teleport-cooldown-seconds",
DEFAULT_FOURTH_TELEPORT_COOLDOWN_SECONDS
),
integer(values, "visitor-unlock-diamond-cost", DEFAULT_VISITOR_UNLOCK_DIAMOND_COST)
integer(values, "visitor-unlock-diamond-cost", DEFAULT_VISITOR_UNLOCK_DIAMOND_COST),
stringSet(values, "base-unlock-materials", Set.of("GRASS_BLOCK", "DIRT")),
string(values, "stone-expansion-material", "STONE"),
string(values, "deepslate-expansion-material", "DEEPSLATE"),
string(values, "obsidian-expansion-material", "OBSIDIAN"),
string(values, "visitor-currency-material", "DIAMOND"),
stringSet(values, "cooldown-excluded-materials", Set.of()),
integer(values, "boss-bar-duration-ticks", DEFAULT_BOSS_BAR_DURATION_TICKS),
integer(values, "navigation-particle-count", DEFAULT_NAVIGATION_PARTICLE_COUNT),
integer(values, "title-fade-in-ticks", DEFAULT_TITLE_FADE_IN_TICKS),
integer(values, "title-stay-ticks", DEFAULT_TITLE_STAY_TICKS),
integer(values, "title-fade-out-ticks", DEFAULT_TITLE_FADE_OUT_TICKS)
);
}
private static String string(Map<String, ?> values, String key, String defaultValue) {
Object value = values.get(key);
return value == null ? defaultValue : normalizedName(value.toString(), key);
}
private static Set<String> stringSet(
Map<String, ?> values,
String key,
Set<String> defaultValue
) {
Object value = values.get(key);
if (value == null) {
return defaultValue;
}
if (!(value instanceof List<?> list)) {
throw new IllegalArgumentException(key + " must be a list");
}
return list.stream().map(Object::toString).collect(Collectors.toUnmodifiableSet());
}
private static Set<String> normalizedSet(Set<String> values, String key, boolean allowEmpty) {
if (values == null || (!allowEmpty && values.isEmpty())) {
throw new IllegalArgumentException(key + " must not be empty");
}
return values.stream()
.map(value -> normalizedName(value, key))
.collect(Collectors.toUnmodifiableSet());
}
private static String normalizedName(String value, String key) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(key + " contains an empty material name");
}
return value.trim().toUpperCase(java.util.Locale.ROOT);
}
private static int integer(Map<String, ?> values, String key, int defaultValue) {
long value = longInteger(values, key, defaultValue);
if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) {
@@ -0,0 +1,19 @@
package games.dmg.spigotbase;
import java.util.Objects;
public final class PluginSettingsProvider {
private volatile PluginSettings current;
public PluginSettingsProvider(PluginSettings initial) {
current = Objects.requireNonNull(initial, "initial");
}
public PluginSettings current() {
return current;
}
public void update(PluginSettings updated) {
current = Objects.requireNonNull(updated, "updated");
}
}
@@ -0,0 +1,35 @@
package games.dmg.spigotbase;
import java.util.stream.Stream;
import org.bukkit.Material;
final class PluginSettingsValidator {
private PluginSettingsValidator() {
}
static PluginSettings validateMaterials(PluginSettings settings) {
Stream.concat(
settings.baseUnlockMaterials().stream(),
Stream.of(
settings.stoneExpansionMaterial(),
settings.deepslateExpansionMaterial(),
settings.obsidianExpansionMaterial()
)
).forEach(name -> {
Material material = Material.matchMaterial(name);
if (material == null || !material.isBlock()) {
throw new IllegalArgumentException(name + " must identify a block material");
}
});
for (String name : settings.cooldownExcludedMaterials()) {
if (Material.matchMaterial(name) == null) {
throw new IllegalArgumentException(name + " is not a known material");
}
}
Material currency = Material.matchMaterial(settings.visitorCurrencyMaterial());
if (currency == null || currency.isAir()) {
throw new IllegalArgumentException("visitor-currency-material is invalid");
}
return settings;
}
}
@@ -0,0 +1,10 @@
package games.dmg.spigotbase;
public enum ProgressCounter {
GRASS_DIRT,
STONE,
DEEPSLATE,
OBSIDIAN,
PLACEMENTS,
BASE_BREAKS
}
@@ -1,9 +1,13 @@
package games.dmg.spigotbase;
public final class SecondaryProgressionService {
private final PluginSettings settings;
private final PluginSettingsProvider settings;
public SecondaryProgressionService(PluginSettings settings) {
this(new PluginSettingsProvider(settings));
}
public SecondaryProgressionService(PluginSettingsProvider settings) {
this.settings = settings;
}
@@ -12,7 +16,7 @@ public final class SecondaryProgressionService {
return ProgressionUpdate.unchanged(player);
}
long count = increment(player.stoneBroken());
boolean unlocked = count >= settings.stoneExpansionBlocks();
boolean unlocked = count >= settings.current().stoneExpansionBlocks();
return new ProgressionUpdate(
player.withSizeProgress(
count, player.deepslateBroken(), player.obsidianBroken(), unlocked ? 1 : 0
@@ -26,7 +30,7 @@ public final class SecondaryProgressionService {
return ProgressionUpdate.unchanged(player);
}
long count = increment(player.deepslateBroken());
boolean unlocked = count >= settings.deepslateExpansionBlocks();
boolean unlocked = count >= settings.current().deepslateExpansionBlocks();
return new ProgressionUpdate(
player.withSizeProgress(
player.stoneBroken(), count, player.obsidianBroken(), unlocked ? 2 : 1
@@ -40,7 +44,7 @@ public final class SecondaryProgressionService {
return ProgressionUpdate.unchanged(player);
}
long count = increment(player.obsidianBroken());
boolean unlocked = count >= settings.obsidianExpansionBlocks();
boolean unlocked = count >= settings.current().obsidianExpansionBlocks();
return new ProgressionUpdate(
player.withSizeProgress(
player.stoneBroken(), player.deepslateBroken(), count, unlocked ? 3 : 2
@@ -9,7 +9,7 @@ import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;
public final class SpigotBasePlugin extends JavaPlugin {
private PluginSettings settings;
private PluginSettingsProvider settingsProvider;
private BaseStateManager stateManager;
private BaseProgressListener progressListener;
private BaseFlightController flightController;
@@ -20,7 +20,9 @@ public final class SpigotBasePlugin extends JavaPlugin {
saveDefaultConfig();
try {
Map<String, Object> values = getConfig().getValues(false);
settings = PluginSettings.from(values);
settingsProvider = new PluginSettingsProvider(
PluginSettingsValidator.validateMaterials(PluginSettings.from(values))
);
stateManager = new BaseStateManager(
new YamlBaseStateRepository(getDataFolder().toPath().resolve("state.yml")),
getLogger()
@@ -31,13 +33,13 @@ public final class SpigotBasePlugin extends JavaPlugin {
return;
}
BaseService baseService = new BaseService(settings);
BaseProgressionService progressionService = new BaseProgressionService(settings);
BaseService baseService = new BaseService(settingsProvider);
BaseProgressionService progressionService = new BaseProgressionService(settingsProvider);
SecondaryProgressionService secondaryProgressionService =
new SecondaryProgressionService(settings);
new SecondaryProgressionService(settingsProvider);
TeleportProgressionService teleportProgressionService =
new TeleportProgressionService(settings);
BaseBoundsService boundsService = new BaseBoundsService(settings);
new TeleportProgressionService(settingsProvider);
BaseBoundsService boundsService = new BaseBoundsService(settingsProvider);
progressListener = new BaseProgressListener(
this,
stateManager,
@@ -45,16 +47,16 @@ public final class SpigotBasePlugin extends JavaPlugin {
secondaryProgressionService,
teleportProgressionService,
boundsService,
settings
settingsProvider
);
flightController = new BaseFlightController(
getServer(), stateManager, secondaryProgressionService, boundsService, settings
getServer(), stateManager, secondaryProgressionService, boundsService, settingsProvider
);
VisitorPolicy visitorPolicy = new VisitorPolicy();
teleportManager = new BaseTeleportManager(
this,
stateManager,
new TeleportPolicy(settings),
new TeleportPolicy(settingsProvider),
visitorPolicy,
new SafeBaseDestination(),
Clock.systemUTC()
@@ -64,9 +66,11 @@ public final class SpigotBasePlugin extends JavaPlugin {
command("setbase").setExecutor(new SetBaseCommand(stateManager, baseService, Clock.systemUTC()));
command("base").setExecutor(
new BaseCommand(teleportManager, stateManager, visitorPolicy, settings)
new BaseCommand(teleportManager, stateManager, visitorPolicy, settingsProvider)
);
command("baseprogress").setExecutor(
new BaseProgressCommand(stateManager, settingsProvider)
);
command("baseprogress").setExecutor(new BaseProgressCommand(stateManager, settings));
command("basenavigation").setExecutor(new BaseNavigationCommand(stateManager));
command("baseflight").setExecutor(new BaseFlightCommand(stateManager, flightController));
command("basevisitors").setExecutor(new BaseVisitorsCommand(stateManager));
@@ -74,11 +78,19 @@ public final class SpigotBasePlugin extends JavaPlugin {
command("gotobase").setExecutor(goToBaseCommand);
command("gotobase").setTabCompleter(goToBaseCommand);
command("baseadmin").setExecutor(
new BaseAdminCommand(stateManager, new AdminProgressionService())
new BaseAdminCommand(
this,
stateManager,
new AdminProgressionService(settingsProvider),
settingsProvider
)
);
getServer().getScheduler().runTaskTimer(
this, new BaseNavigationController(getServer(), stateManager), 10L, 10L
this,
new BaseNavigationController(getServer(), stateManager, settingsProvider),
10L,
10L
);
getServer().getScheduler().runTaskTimer(this, flightController, 5L, 5L);
getServer().getScheduler().runTaskTimer(this, stateManager::saveIfDirty, 600L, 600L);
@@ -102,10 +114,10 @@ public final class SpigotBasePlugin extends JavaPlugin {
}
PluginSettings settings() {
if (settings == null) {
if (settingsProvider == null) {
throw new IllegalStateException("Plugin settings are unavailable");
}
return settings;
return settingsProvider.current();
}
private PluginCommand command(String name) {
@@ -5,17 +5,21 @@ import java.time.Instant;
import java.util.Optional;
public final class TeleportPolicy {
private final PluginSettings settings;
private final PluginSettingsProvider settings;
public TeleportPolicy(PluginSettings settings) {
this(new PluginSettingsProvider(settings));
}
public TeleportPolicy(PluginSettingsProvider settings) {
this.settings = settings;
}
public Duration warmup(PlayerState player) {
return switch (player.warmupLevel()) {
case 0 -> Duration.ofSeconds(settings.initialWarmupSeconds());
case 1 -> Duration.ofSeconds(settings.secondWarmupSeconds());
case 2 -> Duration.ofSeconds(settings.thirdWarmupSeconds());
case 0 -> Duration.ofSeconds(settings.current().initialWarmupSeconds());
case 1 -> Duration.ofSeconds(settings.current().secondWarmupSeconds());
case 2 -> Duration.ofSeconds(settings.current().thirdWarmupSeconds());
case 3 -> Duration.ZERO;
default -> throw new IllegalArgumentException("unknown warm-up level");
};
@@ -23,10 +27,10 @@ public final class TeleportPolicy {
public Duration cooldown(PlayerState player) {
return switch (player.cooldownLevel()) {
case 0 -> Duration.ofSeconds(settings.initialTeleportCooldownSeconds());
case 1 -> Duration.ofSeconds(settings.secondTeleportCooldownSeconds());
case 2 -> Duration.ofSeconds(settings.thirdTeleportCooldownSeconds());
case 3 -> Duration.ofSeconds(settings.fourthTeleportCooldownSeconds());
case 0 -> Duration.ofSeconds(settings.current().initialTeleportCooldownSeconds());
case 1 -> Duration.ofSeconds(settings.current().secondTeleportCooldownSeconds());
case 2 -> Duration.ofSeconds(settings.current().thirdTeleportCooldownSeconds());
case 3 -> Duration.ofSeconds(settings.current().fourthTeleportCooldownSeconds());
case 4 -> Duration.ZERO;
default -> throw new IllegalArgumentException("unknown cooldown level");
};
@@ -1,9 +1,13 @@
package games.dmg.spigotbase;
public final class TeleportProgressionService {
private final PluginSettings settings;
private final PluginSettingsProvider settings;
public TeleportProgressionService(PluginSettings settings) {
this(new PluginSettingsProvider(settings));
}
public TeleportProgressionService(PluginSettingsProvider settings) {
this.settings = settings;
}
@@ -13,7 +17,7 @@ public final class TeleportProgressionService {
}
long placements = increment(player.blocksPlacedInBase());
int baseLevel = player.baseLevel();
if (baseLevel == 2 && placements >= settings.teleportUnlockPlacements()) {
if (baseLevel == 2 && placements >= settings.current().teleportUnlockPlacements()) {
baseLevel = 3;
}
int warmupLevel = baseLevel >= 3 ? warmupLevel(placements) : 0;
@@ -56,29 +60,29 @@ public final class TeleportProgressionService {
}
private int warmupLevel(long placements) {
if (placements >= settings.instantWarmupPlacements()) {
if (placements >= settings.current().instantWarmupPlacements()) {
return 3;
}
if (placements >= settings.thirdWarmupPlacements()) {
if (placements >= settings.current().thirdWarmupPlacements()) {
return 2;
}
if (placements >= settings.secondWarmupPlacements()) {
if (placements >= settings.current().secondWarmupPlacements()) {
return 1;
}
return 0;
}
private int cooldownLevel(long breaks) {
if (breaks >= settings.instantCooldownBreaks()) {
if (breaks >= settings.current().instantCooldownBreaks()) {
return 4;
}
if (breaks >= settings.thirdCooldownBreaks()) {
if (breaks >= settings.current().thirdCooldownBreaks()) {
return 3;
}
if (breaks >= settings.secondCooldownBreaks()) {
if (breaks >= settings.current().secondCooldownBreaks()) {
return 2;
}
if (breaks >= settings.firstCooldownBreaks()) {
if (breaks >= settings.current().firstCooldownBreaks()) {
return 1;
}
return 0;
@@ -38,7 +38,8 @@ public final class YamlBaseStateRepository {
if (parent != null) {
Files.createDirectories(parent);
}
YamlConfiguration yaml = new YamlConfiguration();
YamlConfiguration yaml = loadForSave();
removePlayersAbsentFrom(state, yaml);
savePlayers(yaml, state.players());
Path temporary = Files.createTempFile(parent, "spigot-base-state-", ".yml");
@@ -59,6 +60,38 @@ public final class YamlBaseStateRepository {
}
}
private YamlConfiguration loadForSave() throws IOException {
YamlConfiguration yaml = new YamlConfiguration();
if (!Files.exists(stateFile)) {
return yaml;
}
try {
yaml.load(stateFile.toFile());
return yaml;
} catch (InvalidConfigurationException exception) {
throw new IOException("state file is not valid YAML", exception);
}
}
private static void removePlayersAbsentFrom(
PersistentState state,
YamlConfiguration yaml
) {
ConfigurationSection section = yaml.getConfigurationSection("players");
if (section == null) {
return;
}
for (String key : section.getKeys(false)) {
try {
if (!state.players().containsKey(UUID.fromString(key))) {
yaml.set("players." + key, null);
}
} catch (IllegalArgumentException exception) {
yaml.set("players." + key, null);
}
}
}
private static Map<UUID, PlayerState> loadPlayers(YamlConfiguration yaml) {
Map<UUID, PlayerState> players = new HashMap<>();
ConfigurationSection section = yaml.getConfigurationSection("players");