feat(admin): complete configurable progression
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user