diff --git a/src/main/java/dev/loki/lomines/ComponentInitializer.java b/src/main/java/dev/loki/lomines/ComponentInitializer.java index 3a6a43b..e8c5b7d 100644 --- a/src/main/java/dev/loki/lomines/ComponentInitializer.java +++ b/src/main/java/dev/loki/lomines/ComponentInitializer.java @@ -1,7 +1,7 @@ package dev.loki.lomines; -import dev.loki.lomines.core.MineTicker; -import dev.loki.lomines.core.Mines; +import dev.loki.lomines.core.mine.MineTicker; +import dev.loki.lomines.core.mine.Mines; import dev.loki.lomines.data.stats.StatsManager; import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.wand.GroupWandManager; diff --git a/src/main/java/dev/loki/lomines/LoMinesPlugin.java b/src/main/java/dev/loki/lomines/LoMinesPlugin.java index 9fa705b..495d3d0 100644 --- a/src/main/java/dev/loki/lomines/LoMinesPlugin.java +++ b/src/main/java/dev/loki/lomines/LoMinesPlugin.java @@ -1,7 +1,7 @@ package dev.loki.lomines; -import dev.loki.lomines.core.MineTicker; -import dev.loki.lomines.core.Mines; +import dev.loki.lomines.core.mine.MineTicker; +import dev.loki.lomines.core.mine.Mines; import dev.loki.lomines.data.stats.StatsManager; import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.wand.GroupWandManager; diff --git a/src/main/java/dev/loki/lomines/RegistrationManager.java b/src/main/java/dev/loki/lomines/RegistrationManager.java index bb7feea..2c18973 100644 --- a/src/main/java/dev/loki/lomines/RegistrationManager.java +++ b/src/main/java/dev/loki/lomines/RegistrationManager.java @@ -1,10 +1,10 @@ package dev.loki.lomines; -import dev.loki.lomines.command.AdminCommands; -import dev.loki.lomines.command.LoMinesTabCompleter; -import dev.loki.lomines.command.MaskCommands; -import dev.loki.lomines.command.PlayerCommands; -import dev.loki.lomines.command.StatsCommands; +import dev.loki.lomines.command.admin.AdminCommands; +import dev.loki.lomines.command.admin.MaskCommands; +import dev.loki.lomines.command.admin.StatsCommands; +import dev.loki.lomines.command.common.LoMinesTabCompleter; +import dev.loki.lomines.command.player.PlayerCommands; import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.listener.BlockBreakListener; import dev.loki.lomines.listener.GroupGuiListener; diff --git a/src/main/java/dev/loki/lomines/command/admin/AdminCommands.java b/src/main/java/dev/loki/lomines/command/admin/AdminCommands.java new file mode 100644 index 0000000..0aa0f73 --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/AdminCommands.java @@ -0,0 +1,161 @@ +package dev.loki.lomines.command.admin; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.core.mine.Mines; +import dev.loki.lomines.gui.mine.MineEditGui; +import dev.loki.lomines.util.ErrorHandler; +import dev.loki.lomines.util.MessageFormatter; +import dev.lolib.commands.annotation.Arg; +import dev.lolib.commands.annotation.Command; +import dev.lolib.commands.annotation.Subcommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.io.IOException; +import java.util.Collection; + +/** + * Admin commands for mine management. + * Provides commands for creating, deleting, resetting, reloading, listing, and editing mines. + */ +@Command(value = "lm", permission = "lomines.use") +public class AdminCommands { + + private final LoMinesPlugin plugin; + private final Mines mines; + private final ErrorHandler errorHandler; + + public AdminCommands(LoMinesPlugin plugin) { + this.plugin = plugin; + this.mines = plugin.getMines(); + this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); + } + + /** + * Creates a new mine with the given name. + * Usage: /lm create + */ + @Subcommand(value = "create", permission = "lomines.admin.create") + public void create(CommandSender sender, @Arg("name") String name) { + try { + mines.create(name); + sender.sendMessage(MessageFormatter.success("Successfully created mine: " + name)); + sender.sendMessage(Component.text( + "Edit selection & contents in plugins/LoMines/mines/" + name + ".yml — " + + "for shape-based ore (not full cuboid), paint mask.marker blocks, then: /lm maskscan " + name)); + if (sender instanceof Player player) { + sender.sendMessage(Component.text( + "Or use: /lm edit " + name + " to open GUI editor", NamedTextColor.YELLOW)); + } + } catch (IllegalArgumentException e) { + errorHandler.handleNotFound(sender, "Mine already exists", name); + } catch (IOException e) { + errorHandler.handleError(sender, "Failed to create mine: " + e.getMessage(), + "Failed to create mine " + name, e); + } + } + + /** + * Deletes an existing mine. + * Usage: /lm delete + */ + @Subcommand(value = "delete", permission = "lomines.admin.delete") + public void delete(CommandSender sender, @Arg("name") String name) { + try { + mines.delete(name); + sender.sendMessage(MessageFormatter.success("Successfully deleted mine: " + name)); + } catch (IllegalArgumentException e) { + errorHandler.handleNotFound(sender, "Mine", name); + } catch (IOException e) { + errorHandler.handleError(sender, "Failed to delete mine: " + e.getMessage(), + "Failed to delete mine " + name, e); + } + } + + /** + * Opens the GUI editor for a mine. + * Usage: /lm edit + */ + @Subcommand(value = "edit", permission = "lomines.admin.edit") + public void edit(CommandSender sender, @Arg("mine") String mineName) { + if (!(sender instanceof Player player)) { + sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); + return; + } + + Mine mine = mines.find(mineName).orElse(null); + if (mine == null) { + errorHandler.handleNotFound(sender, "Mine", mineName); + return; + } + + MineEditGui.open(plugin, player, mineName); + } + + /** + * Resets a mine by refilling all its regions. + * Usage: /lm reset [silent] + */ + @Subcommand(value = "reset", permission = "lomines.admin.reset") + public void reset( + CommandSender sender, + @Arg("mine") String mineName, + @Arg(value = "silent", optional = true, defaultValue = "false") boolean silent + ) { + try { + Mine mine = mines.get(mineName); + mine.reset(silent); + sender.sendMessage(MessageFormatter.success("Resetting mine: " + mineName)); + } catch (IllegalArgumentException e) { + errorHandler.handleNotFound(sender, "Mine", mineName); + } + } + + /** + * Reloads all mine configurations from disk. + * Usage: /lm reload + */ + @Subcommand(value = "reload", permission = "lomines.admin.reload") + public void reload(CommandSender sender) { + try { + mines.getAll().forEach(Mine::stop); + mines.loadAll(); + int count = mines.getAll().size(); + sender.sendMessage(MessageFormatter.success("Successfully reloaded " + count + " mine(s)")); + } catch (IOException e) { + errorHandler.handleError(sender, "Failed to reload mines: " + e.getMessage(), + "Failed to reload mines", e); + } + } + + /** + * Lists all active mines. + * Usage: /lm list + */ + @Subcommand(value = "list", permission = "lomines.admin.list") + public void list(CommandSender sender) { + Collection allMines = mines.getAll(); + + if (allMines.isEmpty()) { + sender.sendMessage(Component.text("No mines found")); + return; + } + + sender.sendMessage(Component.text("=== Mines (" + allMines.size() + ") ===")); + + for (Mine mine : allMines) { + String status = formatMineStatus(mine); + sender.sendMessage(Component.text("- " + mine.getName() + " " + status)); + } + } + + private String formatMineStatus(Mine mine) { + int blocks = mine.getBlocks(); + int total = mine.getTotalVolume(); + double percent = mine.getPercentFilled(); + return String.format("(%d/%d blocks, %.1f%%)", blocks, total, percent); + } +} diff --git a/src/main/java/dev/loki/lomines/command/MaskCommands.java b/src/main/java/dev/loki/lomines/command/admin/MaskCommands.java similarity index 92% rename from src/main/java/dev/loki/lomines/command/MaskCommands.java rename to src/main/java/dev/loki/lomines/command/admin/MaskCommands.java index b7c8758..c91fbe5 100644 --- a/src/main/java/dev/loki/lomines/command/MaskCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/MaskCommands.java @@ -1,8 +1,8 @@ -package dev.loki.lomines.command; +package dev.loki.lomines.command.admin; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mines; -import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.core.mine.Mines; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.util.ErrorHandler; import dev.loki.lomines.util.MessageFormatter; import dev.lolib.commands.annotation.Arg; diff --git a/src/main/java/dev/loki/lomines/command/StatsCommands.java b/src/main/java/dev/loki/lomines/command/admin/StatsCommands.java similarity index 98% rename from src/main/java/dev/loki/lomines/command/StatsCommands.java rename to src/main/java/dev/loki/lomines/command/admin/StatsCommands.java index c3e262d..d6d293a 100644 --- a/src/main/java/dev/loki/lomines/command/StatsCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/StatsCommands.java @@ -1,8 +1,8 @@ -package dev.loki.lomines.command; +package dev.loki.lomines.command.admin; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mine; -import dev.loki.lomines.core.Mines; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.core.mine.Mines; import dev.loki.lomines.data.stats.Leaderboard; import dev.loki.lomines.data.stats.LeaderboardEntry; import dev.loki.lomines.data.stats.PlayerStats; diff --git a/src/main/java/dev/loki/lomines/command/AdminCommands.java b/src/main/java/dev/loki/lomines/command/admin/TeleportCommands.java similarity index 50% rename from src/main/java/dev/loki/lomines/command/AdminCommands.java rename to src/main/java/dev/loki/lomines/command/admin/TeleportCommands.java index cf61e41..5bfdb74 100644 --- a/src/main/java/dev/loki/lomines/command/AdminCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/TeleportCommands.java @@ -1,16 +1,14 @@ -package dev.loki.lomines.command; +package dev.loki.lomines.command.admin; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mine; -import dev.loki.lomines.core.Mines; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.core.mine.Mines; import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; import dev.loki.lomines.data.config.teleport.TeleportConfig; -import dev.loki.lomines.gui.MineEditGui; import dev.loki.lomines.util.ErrorHandler; import dev.loki.lomines.util.MessageFormatter; import dev.lolib.commands.annotation.Arg; -import dev.lolib.commands.annotation.Command; import dev.lolib.commands.annotation.Subcommand; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -18,142 +16,19 @@ import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import java.io.IOException; -import java.util.Collection; - /** - * Admin commands for mine management. - * Provides commands for creating, deleting, resetting, reloading, listing, and editing mines. + * Commands for managing teleport and spawn locations for mines. */ -@Command(value = "lm", permission = "lomines.use") -public class AdminCommands { +class TeleportCommands { private final LoMinesPlugin plugin; private final Mines mines; private final ErrorHandler errorHandler; - public AdminCommands(LoMinesPlugin plugin) { + TeleportCommands(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) { this.plugin = plugin; - this.mines = plugin.getMines(); - this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); - } - - /** - * Creates a new mine with the given name. - * Usage: /lm create - */ - @Subcommand(value = "create", permission = "lomines.admin.create") - public void create(CommandSender sender, @Arg("name") String name) { - try { - mines.create(name); - sender.sendMessage(MessageFormatter.success("Successfully created mine: " + name)); - sender.sendMessage(Component.text( - "Edit selection & contents in plugins/LoMines/mines/" + name + ".yml — " - + "for shape-based ore (not full cuboid), paint mask.marker blocks, then: /lm maskscan " + name)); - if (sender instanceof Player player) { - sender.sendMessage(Component.text( - "Or use: /lm edit " + name + " to open GUI editor", NamedTextColor.YELLOW)); - } - } catch (IllegalArgumentException e) { - errorHandler.handleNotFound(sender, "Mine already exists", name); - } catch (IOException e) { - errorHandler.handleError(sender, "Failed to create mine: " + e.getMessage(), - "Failed to create mine " + name, e); - } - } - - /** - * Deletes an existing mine. - * Usage: /lm delete - */ - @Subcommand(value = "delete", permission = "lomines.admin.delete") - public void delete(CommandSender sender, @Arg("name") String name) { - try { - mines.delete(name); - sender.sendMessage(MessageFormatter.success("Successfully deleted mine: " + name)); - } catch (IllegalArgumentException e) { - errorHandler.handleNotFound(sender, "Mine", name); - } catch (IOException e) { - errorHandler.handleError(sender, "Failed to delete mine: " + e.getMessage(), - "Failed to delete mine " + name, e); - } - } - - /** - * Opens the GUI editor for a mine. - * Usage: /lm edit - */ - @Subcommand(value = "edit", permission = "lomines.admin.edit") - public void edit(CommandSender sender, @Arg("mine") String mineName) { - if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); - return; - } - - Mine mine = mines.find(mineName).orElse(null); - if (mine == null) { - errorHandler.handleNotFound(sender, "Mine", mineName); - return; - } - - MineEditGui.open(plugin, player, mineName); - } - - /** - * Resets a mine by refilling all its regions. - * Usage: /lm reset [silent] - */ - @Subcommand(value = "reset", permission = "lomines.admin.reset") - public void reset( - CommandSender sender, - @Arg("mine") String mineName, - @Arg(value = "silent", optional = true, defaultValue = "false") boolean silent - ) { - try { - Mine mine = mines.get(mineName); - mine.reset(silent); - sender.sendMessage(MessageFormatter.success("Resetting mine: " + mineName)); - } catch (IllegalArgumentException e) { - errorHandler.handleNotFound(sender, "Mine", mineName); - } - } - - /** - * Reloads all mine configurations from disk. - * Usage: /lm reload - */ - @Subcommand(value = "reload", permission = "lomines.admin.reload") - public void reload(CommandSender sender) { - try { - mines.getAll().forEach(Mine::stop); - mines.loadAll(); - int count = mines.getAll().size(); - sender.sendMessage(MessageFormatter.success("Successfully reloaded " + count + " mine(s)")); - } catch (IOException e) { - errorHandler.handleError(sender, "Failed to reload mines: " + e.getMessage(), - "Failed to reload mines", e); - } - } - - /** - * Lists all active mines. - * Usage: /lm list - */ - @Subcommand(value = "list", permission = "lomines.admin.list") - public void list(CommandSender sender) { - Collection allMines = mines.getAll(); - - if (allMines.isEmpty()) { - sender.sendMessage(Component.text("No mines found")); - return; - } - - sender.sendMessage(Component.text("=== Mines (" + allMines.size() + ") ===")); - - for (Mine mine : allMines) { - String status = formatMineStatus(mine); - sender.sendMessage(Component.text("- " + mine.getName() + " " + status)); - } + this.mines = mines; + this.errorHandler = errorHandler; } /** @@ -161,7 +36,7 @@ public class AdminCommands { * Usage: /lm setteleport */ @Subcommand(value = "setteleport", permission = "lomines.admin.setteleport") - public void setTeleport(CommandSender sender, @Arg("mine") String mineName) { + void setTeleport(CommandSender sender, @Arg("mine") String mineName) { if (!(sender instanceof Player player)) { sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); return; @@ -174,7 +49,6 @@ public class AdminCommands { return; } - // Get current player location and update config var config = mine.getConfig(); var newTeleport = TeleportConfig.at(player.getLocation()); @@ -203,7 +77,7 @@ public class AdminCommands { * Usage: /lm setspawn */ @Subcommand(value = "setspawn", permission = "lomines.admin.setspawn") - public void setSpawn(CommandSender sender, @Arg("mine") String mineName) { + void setSpawn(CommandSender sender, @Arg("mine") String mineName) { if (!(sender instanceof Player player)) { sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); return; @@ -216,7 +90,6 @@ public class AdminCommands { return; } - // Get current player location and update config var config = mine.getConfig(); var newSpawn = PlayerSpawnConfig.at(player.getLocation()); @@ -246,7 +119,7 @@ public class AdminCommands { * Usage: /lm clearspawn */ @Subcommand(value = "clearspawn", permission = "lomines.admin.setspawn") - public void clearSpawn(CommandSender sender, @Arg("mine") String mineName) { + void clearSpawn(CommandSender sender, @Arg("mine") String mineName) { try { Mine mine = mines.find(mineName).orElse(null); if (mine == null) { @@ -275,22 +148,8 @@ public class AdminCommands { } } - /** - * Formats location for display. - */ private String formatLocation(Location loc) { return String.format("%.1f, %.1f, %.1f in %s", loc.getX(), loc.getY(), loc.getZ(), loc.getWorld().getName()); } - - /** - * Formats mine status information for display. - */ - private String formatMineStatus(Mine mine) { - int blocks = mine.getBlocks(); - int total = mine.getTotalVolume(); - double percent = mine.getPercentFilled(); - - return String.format("(%d/%d blocks, %.1f%%)", blocks, total, percent); - } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/lomines/command/LoMinesTabCompleter.java b/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java similarity index 99% rename from src/main/java/dev/loki/lomines/command/LoMinesTabCompleter.java rename to src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java index 18a1080..0ac51f4 100644 --- a/src/main/java/dev/loki/lomines/command/LoMinesTabCompleter.java +++ b/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.command; +package dev.loki.lomines.command.common; import dev.loki.lomines.LoMinesPlugin; import org.bukkit.command.Command; diff --git a/src/main/java/dev/loki/lomines/command/package-info.java b/src/main/java/dev/loki/lomines/command/common/package-info.java similarity index 93% rename from src/main/java/dev/loki/lomines/command/package-info.java rename to src/main/java/dev/loki/lomines/command/common/package-info.java index 11e3c36..a0db9a7 100644 --- a/src/main/java/dev/loki/lomines/command/package-info.java +++ b/src/main/java/dev/loki/lomines/command/common/package-info.java @@ -10,4 +10,4 @@ *
  • {@code WorldGuardCommands} - WorldGuard integration commands
  • * */ -package dev.loki.lomines.command; +package dev.loki.lomines.command.common; diff --git a/src/main/java/dev/loki/lomines/command/PlayerCommands.java b/src/main/java/dev/loki/lomines/command/player/PlayerCommands.java similarity index 98% rename from src/main/java/dev/loki/lomines/command/PlayerCommands.java rename to src/main/java/dev/loki/lomines/command/player/PlayerCommands.java index 4887f6f..873d1d8 100644 --- a/src/main/java/dev/loki/lomines/command/PlayerCommands.java +++ b/src/main/java/dev/loki/lomines/command/player/PlayerCommands.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.command; +package dev.loki.lomines.command.player; import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.util.ErrorHandler; diff --git a/src/main/java/dev/loki/lomines/core/package-info.java b/src/main/java/dev/loki/lomines/core/common/package-info.java similarity index 90% rename from src/main/java/dev/loki/lomines/core/package-info.java rename to src/main/java/dev/loki/lomines/core/common/package-info.java index fb772fb..2228c11 100644 --- a/src/main/java/dev/loki/lomines/core/package-info.java +++ b/src/main/java/dev/loki/lomines/core/common/package-info.java @@ -8,4 +8,4 @@ *
  • {@code Mine} - Representation of a single mine with its configuration and state
  • * */ -package dev.loki.lomines.core; +package dev.loki.lomines.core.common; diff --git a/src/main/java/dev/loki/lomines/core/Mine.java b/src/main/java/dev/loki/lomines/core/mine/Mine.java similarity index 56% rename from src/main/java/dev/loki/lomines/core/Mine.java rename to src/main/java/dev/loki/lomines/core/mine/Mine.java index 6f95008..800faaa 100644 --- a/src/main/java/dev/loki/lomines/core/Mine.java +++ b/src/main/java/dev/loki/lomines/core/mine/Mine.java @@ -1,6 +1,7 @@ -package dev.loki.lomines.core; +package dev.loki.lomines.core.mine; import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.service.MineFileManager; import dev.loki.lomines.block.BlockSetter; import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.block.BlockKey; @@ -10,6 +11,7 @@ import dev.loki.lomines.handler.MineBlockHandler; import dev.loki.lomines.handler.MineResetHandler; import dev.loki.lomines.util.location.BlockKeys; import dev.loki.lomines.util.location.Cuboid; +import dev.loki.lomines.util.location.LocationParser; import dev.lolib.scheduler.ScheduledTask; import dev.lolib.scheduler.Scheduler; import org.bukkit.Location; @@ -26,73 +28,57 @@ import java.util.concurrent.atomic.AtomicInteger; */ public final class Mine { - // Immutable configuration private final String name; private final List regions; private final MineConfig config; private final BlockSetter blockSetter; private final LoMinesPlugin plugin; private final int totalVolume; - /** - * Block keys for {@link FillMode#MASK}; empty when not mask mode. - */ private final Set maskBlockKeys; - - // Mutable state (thread-safe) private final AtomicInteger blocks; private final AtomicInteger ticks; - - // Handlers private final MineResetHandler resetHandler; private final MineBlockHandler blockHandler; private final ActionBarHandler actionBarHandler; - - // Tasks private ScheduledTask actionBarTask; - /** - * Creates a new mine instance with new configuration system. - */ public Mine(String name, MineConfig config, LoMinesPlugin plugin) { this.name = name; this.config = config; this.plugin = plugin; this.regions = config.region().regions(); this.blockSetter = createBlockSetter(config, plugin); - - if (config.blocks().fillMode() == FillMode.MASK) { - this.maskBlockKeys = new HashSet<>(); - if (config.blocks().mask() != null) { - for (String pos : config.blocks().mask().positions().keySet()) { - Location loc = parseLocation(pos); - if (loc != null) { - maskBlockKeys.add(BlockKeys.key(loc)); - } - } - } - this.totalVolume = maskBlockKeys.size(); - } else { - this.maskBlockKeys = Set.of(); - this.totalVolume = config.region().totalVolume(); - } + this.maskBlockKeys = initMaskBlockKeys(config); + this.totalVolume = config.blocks().fillMode() == FillMode.MASK + ? maskBlockKeys.size() + : config.region().totalVolume(); this.blocks = new AtomicInteger(Math.max(0, totalVolume)); this.ticks = new AtomicInteger(0); - this.resetHandler = new MineResetHandler(this, plugin); this.blockHandler = new MineBlockHandler(this, plugin); this.actionBarHandler = new ActionBarHandler(this); } - /** - * Starts periodic tasks for this mine. - */ + private Set initMaskBlockKeys(MineConfig config) { + if (config.blocks().fillMode() != FillMode.MASK) { + return Set.of(); + } + Set keys = new HashSet<>(); + if (config.blocks().mask() != null) { + for (String pos : config.blocks().mask().positions().keySet()) { + Location loc = LocationParser.parseSimple(pos); + if (loc != null) { + keys.add(BlockKeys.key(loc)); + } + } + } + return keys; + } + public void start() { startActionBarTask(); } - /** - * Stops all tasks for this mine. - */ public void stop() { if (actionBarTask != null) { actionBarTask.cancel(); @@ -100,116 +86,53 @@ public final class Mine { } } - /** - * Resets the mine by filling all regions with blocks. - */ public void reset(boolean silent) { resetHandler.reset(silent); } - /** - * Handles a block break event in this mine. - */ public void onBlockBreak(Player player, Block block) { blockHandler.handle(player, block); } - /** - * Checks if the given location is within any region of this mine. - */ public boolean contains(Location location) { return config.region().contains(location); } - /** - * Whether breaking this block counts toward mine progress ({@link FillMode#MASK} ignores blocks outside painted cells). - */ public boolean appliesToBlock(Location location) { - if (!contains(location)) { - return false; - } - if (config.blocks().fillMode() != FillMode.MASK) { - return true; - } + if (!contains(location)) return false; + if (config.blocks().fillMode() != FillMode.MASK) return true; return maskBlockKeys.contains(BlockKeys.key(location)); } - /** - * Calculates the total volume of all regions in this mine. - */ public int getTotalVolume() { return totalVolume; } - /** - * Calculates the percentage of blocks remaining in the mine. - */ public double getPercentFilled() { - if (totalVolume == 0) { - return 0.0; - } + if (totalVolume == 0) return 0.0; return (double) blocks.get() / totalVolume * 100.0; } - /** - * Creates the appropriate BlockSetter based on block configuration. - */ private BlockSetter createBlockSetter(MineConfig config, LoMinesPlugin plugin) { var weights = config.blocks().weights(); - if (weights.isEmpty()) { throw new IllegalArgumentException("Mine must have at least one block type"); } - - // Get the first block key to determine the setter type BlockKey firstKey = weights.keySet().iterator().next(); - return switch (firstKey) { - case BlockKey.Oraxen oraxen -> { - // TODO: Enable when OraxenBlockSetter is ready - throw new IllegalArgumentException("Oraxen integration is currently disabled"); - } - case BlockKey.ItemsAdder itemsAdder -> { - // TODO: Enable when ItemsAdderBlockSetter is ready - throw new IllegalArgumentException("ItemsAdder integration is currently disabled"); - } - case BlockKey.Vanilla vanilla -> - new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin); + case BlockKey.Oraxen oraxen -> throw new IllegalArgumentException("Oraxen integration is currently disabled"); + case BlockKey.ItemsAdder itemsAdder -> throw new IllegalArgumentException("ItemsAdder integration is currently disabled"); + case BlockKey.Vanilla vanilla -> new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin); }; } - /** - * Starts the action bar update task. - */ private void startActionBarTask() { - if (!config.ui().actionBarEnabled()) { - return; - } - + if (!config.ui().actionBarEnabled()) return; actionBarTask = Scheduler.get(plugin).runTimer(() -> { actionBarHandler.sendToNearbyPlayers(); }, 10L, 10L); } - private Location parseLocation(String str) { - String[] parts = str.split(";"); - if (parts.length < 4) return null; - - var world = org.bukkit.Bukkit.getWorld(parts[0]); - if (world == null) return null; - - try { - double x = Double.parseDouble(parts[1]); - double y = Double.parseDouble(parts[2]); - double z = Double.parseDouble(parts[3]); - return new Location(world, x, y, z); - } catch (NumberFormatException e) { - return null; - } - } - - // Getters - public String getName() { return name; } diff --git a/src/main/java/dev/loki/lomines/core/mine/MineLoader.java b/src/main/java/dev/loki/lomines/core/mine/MineLoader.java new file mode 100644 index 0000000..2646503 --- /dev/null +++ b/src/main/java/dev/loki/lomines/core/mine/MineLoader.java @@ -0,0 +1,57 @@ +package dev.loki.lomines.core.common; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.core.service.MineFileManager; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; +import dev.loki.lomines.data.config.MineConfig; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +/** + * Loads mine configurations from disk. + */ +final class MineLoader { + + private final LoMinesPlugin plugin; + private final MineFileManager fileManager; + private final MineRepository repository; + + MineLoader(LoMinesPlugin plugin, MineFileManager fileManager, MineRepository repository) { + this.plugin = plugin; + this.fileManager = fileManager; + this.repository = repository; + } + + void loadAll() throws IOException { + repository.clear(); + + Path minesFolder = fileManager.getMinesFolder(); + if (!Files.exists(minesFolder)) { + return; + } + + try (Stream paths = Files.list(minesFolder)) { + paths.filter(path -> path.toString().endsWith(".yml")) + .filter(path -> !path.getFileName().toString().startsWith("_")) + .forEach(this::loadMineFromFile); + } + } + + private void loadMineFromFile(Path configFile) { + try { + String fileName = configFile.getFileName().toString(); + String mineName = fileName.substring(0, fileName.length() - 4); + + MineConfig config = fileManager.loadConfig(mineName); + repository.createAndStart(mineName, config); + + plugin.loLogger().info("Loaded mine: " + mineName); + } catch (IOException | ConfigLoader.ConfigLoadException e) { + plugin.loLogger().error("Failed to load mine from " + configFile.getFileName() + ": " + e.getMessage()); + } + } +} diff --git a/src/main/java/dev/loki/lomines/core/MineTicker.java b/src/main/java/dev/loki/lomines/core/mine/MineTicker.java similarity index 97% rename from src/main/java/dev/loki/lomines/core/MineTicker.java rename to src/main/java/dev/loki/lomines/core/mine/MineTicker.java index bd98efa..59dc3b5 100644 --- a/src/main/java/dev/loki/lomines/core/MineTicker.java +++ b/src/main/java/dev/loki/lomines/core/mine/MineTicker.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.core; +package dev.loki.lomines.core.mine; import dev.loki.lomines.LoMinesPlugin; import dev.lolib.scheduler.ScheduledTask; diff --git a/src/main/java/dev/loki/lomines/core/Mines.java b/src/main/java/dev/loki/lomines/core/mine/Mines.java similarity index 52% rename from src/main/java/dev/loki/lomines/core/Mines.java rename to src/main/java/dev/loki/lomines/core/mine/Mines.java index 49832be..6c08932 100644 --- a/src/main/java/dev/loki/lomines/core/Mines.java +++ b/src/main/java/dev/loki/lomines/core/mine/Mines.java @@ -1,87 +1,49 @@ -package dev.loki.lomines.core; +package dev.loki.lomines.core.mine; import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.common.MineLoader; import dev.loki.lomines.core.service.MaskScanService; import dev.loki.lomines.core.service.MineFileManager; import dev.loki.lomines.core.service.MineRepository; -import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.integration.worldguard.WorldGuardRegionService; import org.bukkit.Location; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Collection; import java.util.Optional; -import java.util.stream.Stream; /** * Registry for all active mines. * Manages the lifecycle of mines including loading, creating, and deleting. - * Updated for new configuration system (v2). */ public final class Mines { private final LoMinesPlugin plugin; private final MineFileManager fileManager; private final MineRepository repository; + private final MineLoader loader; private final MaskScanService maskScanService; private final WorldGuardRegionService worldGuardService; public Mines(LoMinesPlugin plugin) { this.plugin = plugin; - Path minesFolder = plugin.getDataFolder().toPath().resolve("mines"); - this.fileManager = new MineFileManager(minesFolder); + this.fileManager = new MineFileManager(plugin.getDataFolder().toPath().resolve("mines")); this.repository = new MineRepository(plugin, fileManager); + this.loader = new MineLoader(plugin, fileManager, repository); this.maskScanService = new MaskScanService(repository, fileManager); this.worldGuardService = new WorldGuardRegionService(plugin); } - /** - * Loads all mines from the mines/ folder. - * Clears and replaces any previously loaded mines (used for full reload). - */ public void loadAll() throws IOException { - repository.clear(); - - Path minesFolder = fileManager.getMinesFolder(); - if (!Files.exists(minesFolder)) { - return; - } - - try (Stream paths = Files.list(minesFolder)) { - paths.filter(path -> path.toString().endsWith(".yml")) - .filter(path -> !path.getFileName().toString().startsWith("_")) // Skip defaults - .forEach(configFile -> loadMineFromFile(configFile)); - } + loader.loadAll(); } - private void loadMineFromFile(Path configFile) { - try { - String fileName = configFile.getFileName().toString(); - String mineName = fileName.substring(0, fileName.length() - 4); - - MineConfig config = fileManager.loadConfig(mineName); - repository.createAndStart(mineName, config); - - plugin.loLogger().info("Loaded mine: " + mineName); - } catch (IOException | ConfigLoader.ConfigLoadException e) { - plugin.loLogger().error("Failed to load mine from " + configFile.getFileName() + ": " + e.getMessage()); - } - } - - /** - * Creates a new mine with the given name and default cuboid corners. - */ public void create(String name) throws IOException { create(name, null, null); } - /** - * Creates a new mine; optional corners define {@code selection.1} and {@code selection.2} (same world required). - * If WorldGuard integration is enabled, creates a region for the mine. - */ public void create(String name, Location corner1, Location corner2) throws IOException { if (repository.exists(name)) { throw new IllegalArgumentException("Mine already exists: " + name); @@ -100,90 +62,48 @@ public final class Mines { throw new IOException("Failed to parse created mine configuration: " + e.getMessage(), e); } - // Create WorldGuard region if enabled - String regionName = worldGuardService.createRegion(name, config); - if (regionName != null) { - plugin.loLogger().info("Created WorldGuard region '" + regionName + "' for mine: " + name); - } - + worldGuardService.createRegion(name, config); repository.createAndStart(name, config); plugin.loLogger().info("Created mine: " + name); } - /** - * Deletes a mine by name. - * Also deletes the associated WorldGuard region if it exists. - */ public void delete(String name) throws IOException { Mine mine = repository.remove(name); - if (mine == null) { throw new IllegalArgumentException("Mine not found: " + name); } mine.stop(); - - // Delete WorldGuard region if exists - MineConfig config = mine.getConfig(); - boolean wgDeleted = worldGuardService.deleteRegion(name, config); - if (wgDeleted) { - plugin.loLogger().info("Deleted WorldGuard region for mine: " + name); - } - + worldGuardService.deleteRegion(name, mine.getConfig()); fileManager.deleteConfig(name); } - /** - * Gets a mine by name. - * - * @throws IllegalArgumentException if mine not found - */ public Mine get(String name) { return repository.get(name); } - /** - * Finds a mine by name, returning Optional. - */ public Optional find(String name) { return repository.find(name); } - /** - * Returns all mines. - */ public Collection getAll() { return repository.getAll(); } - /** - * Finds a mine that contains the given location. - */ public Optional findByLocation(Location location) { return repository.findByLocation(location); } - /** - * Scans cuboid regions for {@code mask.marker} blocks, writes {@code fill-mode: mask} and positions to disk, then reloads the mine. - * Must run on the main thread (world access). - */ public int scanAndSaveMask(String mineName) throws IOException, ConfigLoader.ConfigLoadException { int count = maskScanService.scanAndSave(mineName); plugin.loLogger().info("Mask scan for '" + mineName + "': " + count + " marker block(s)"); return count; } - /** - * Reloads one mine from disk (stop, replace instance, start tasks). - * Updates WorldGuard region if mine regions changed. - */ public void reloadMine(String name) throws IOException, ConfigLoader.ConfigLoadException { - // Get old config before reload Mine oldMine = repository.find(name).orElse(null); - repository.reload(name); - // Update WorldGuard region if mine exists if (oldMine != null) { Mine newMine = repository.find(name).orElse(null); if (newMine != null) { @@ -192,16 +112,10 @@ public final class Mines { } } - /** - * Returns the WorldGuard region service for direct access. - */ public WorldGuardRegionService getWorldGuardService() { return worldGuardService; } - /** - * Returns the file manager for saving mine configurations. - */ public MineFileManager getFileManager() { return fileManager; } diff --git a/src/main/java/dev/loki/lomines/core/service/MaskScanService.java b/src/main/java/dev/loki/lomines/core/service/MaskScanService.java index 118556a..9c4e7a5 100644 --- a/src/main/java/dev/loki/lomines/core/service/MaskScanService.java +++ b/src/main/java/dev/loki/lomines/core/service/MaskScanService.java @@ -1,7 +1,7 @@ package dev.loki.lomines.core.service; -import dev.loki.lomines.core.Mine; -import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.util.ValidationUtils; import dev.loki.lomines.util.location.Cuboid; diff --git a/src/main/java/dev/loki/lomines/core/service/MineFileManager.java b/src/main/java/dev/loki/lomines/core/service/MineFileManager.java index 4abe321..3b77c63 100644 --- a/src/main/java/dev/loki/lomines/core/service/MineFileManager.java +++ b/src/main/java/dev/loki/lomines/core/service/MineFileManager.java @@ -1,6 +1,6 @@ package dev.loki.lomines.core.service; -import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.block.BlockConfig; import dev.loki.lomines.data.config.block.BlockKey; diff --git a/src/main/java/dev/loki/lomines/core/service/MineRepository.java b/src/main/java/dev/loki/lomines/core/service/MineRepository.java index 9d0f425..28c8224 100644 --- a/src/main/java/dev/loki/lomines/core/service/MineRepository.java +++ b/src/main/java/dev/loki/lomines/core/service/MineRepository.java @@ -1,8 +1,8 @@ package dev.loki.lomines.core.service; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mine; -import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.data.config.MineConfig; import org.bukkit.Location; diff --git a/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java index 638c654..c0e32ff 100644 --- a/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java @@ -1,6 +1,13 @@ package dev.loki.lomines.data.config; -import dev.loki.lomines.data.config.loader.*; +import dev.loki.lomines.data.config.loader.block.BlockConfigLoader; +import dev.loki.lomines.data.config.loader.entity.PlayerSpawnConfigLoader; +import dev.loki.lomines.data.config.loader.entity.TeleportConfigLoader; +import dev.loki.lomines.data.config.loader.region.RegionConfigLoader; +import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader; +import dev.loki.lomines.data.config.loader.reset.ResetConfigLoader; +import dev.loki.lomines.data.config.loader.reward.RewardConfigLoader; +import dev.loki.lomines.data.config.loader.system.UIConfigLoader; import org.bukkit.configuration.file.YamlConfiguration; import java.io.IOException; diff --git a/src/main/java/dev/loki/lomines/data/config/loader/BlockConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/block/BlockConfigLoader.java similarity index 98% rename from src/main/java/dev/loki/lomines/data/config/loader/BlockConfigLoader.java rename to src/main/java/dev/loki/lomines/data/config/loader/block/BlockConfigLoader.java index 16a5023..352d471 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/BlockConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/block/BlockConfigLoader.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.config.loader; +package dev.loki.lomines.data.config.loader.block; import dev.loki.lomines.data.config.block.BlockConfig; import dev.loki.lomines.data.config.block.BlockKey; diff --git a/src/main/java/dev/loki/lomines/data/config/loader/PlayerSpawnConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/entity/PlayerSpawnConfigLoader.java similarity index 97% rename from src/main/java/dev/loki/lomines/data/config/loader/PlayerSpawnConfigLoader.java rename to src/main/java/dev/loki/lomines/data/config/loader/entity/PlayerSpawnConfigLoader.java index f0880e2..f371099 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/PlayerSpawnConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/entity/PlayerSpawnConfigLoader.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.config.loader; +package dev.loki.lomines.data.config.loader.entity; import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; import dev.loki.lomines.util.location.LocationParser; diff --git a/src/main/java/dev/loki/lomines/data/config/loader/TeleportConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/entity/TeleportConfigLoader.java similarity index 97% rename from src/main/java/dev/loki/lomines/data/config/loader/TeleportConfigLoader.java rename to src/main/java/dev/loki/lomines/data/config/loader/entity/TeleportConfigLoader.java index 76a430b..003496b 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/TeleportConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/entity/TeleportConfigLoader.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.config.loader; +package dev.loki.lomines.data.config.loader.entity; import dev.loki.lomines.data.config.teleport.TeleportConfig; import dev.loki.lomines.util.location.LocationParser; diff --git a/src/main/java/dev/loki/lomines/data/config/loader/RegionConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/region/RegionConfigLoader.java similarity index 98% rename from src/main/java/dev/loki/lomines/data/config/loader/RegionConfigLoader.java rename to src/main/java/dev/loki/lomines/data/config/loader/region/RegionConfigLoader.java index e4d71c8..409407e 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/RegionConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/region/RegionConfigLoader.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.config.loader; +package dev.loki.lomines.data.config.loader.region; import dev.loki.lomines.data.config.region.RegionConfig; import dev.loki.lomines.util.location.LocationParser; diff --git a/src/main/java/dev/loki/lomines/data/config/loader/WorldGuardConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/region/WorldGuardConfigLoader.java similarity index 98% rename from src/main/java/dev/loki/lomines/data/config/loader/WorldGuardConfigLoader.java rename to src/main/java/dev/loki/lomines/data/config/loader/region/WorldGuardConfigLoader.java index 5add2f1..021da2b 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/WorldGuardConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/region/WorldGuardConfigLoader.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.config.loader; +package dev.loki.lomines.data.config.loader.region; import dev.loki.lomines.data.config.ConfigLoader; import dev.loki.lomines.integration.worldguard.WorldGuardConfig; diff --git a/src/main/java/dev/loki/lomines/data/config/loader/RewardConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/reward/RewardConfigLoader.java similarity index 98% rename from src/main/java/dev/loki/lomines/data/config/loader/RewardConfigLoader.java rename to src/main/java/dev/loki/lomines/data/config/loader/reward/RewardConfigLoader.java index ec9b7e7..bb4defd 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/RewardConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/reward/RewardConfigLoader.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.config.loader; +package dev.loki.lomines.data.config.loader.reward; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.reward.RewardConfig; diff --git a/src/main/java/dev/loki/lomines/data/config/loader/ResetConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java similarity index 96% rename from src/main/java/dev/loki/lomines/data/config/loader/ResetConfigLoader.java rename to src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java index 5ef7729..d939529 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/ResetConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.config.loader; +package dev.loki.lomines.data.config.loader.system; import dev.loki.lomines.data.config.reset.ResetConfig; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/src/main/java/dev/loki/lomines/data/config/loader/UIConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java similarity index 96% rename from src/main/java/dev/loki/lomines/data/config/loader/UIConfigLoader.java rename to src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java index 4ceafd3..d8ba26b 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/UIConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.config.loader; +package dev.loki.lomines.data.config.loader.system; import dev.loki.lomines.data.config.ui.UIConfig; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/src/main/java/dev/loki/lomines/data/reward/RewardCommandParser.java b/src/main/java/dev/loki/lomines/data/reward/RewardCommandParser.java new file mode 100644 index 0000000..a93ab1b --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/reward/RewardCommandParser.java @@ -0,0 +1,36 @@ +package dev.loki.lomines.data.reward; + +import dev.loki.lomines.data.config.parser.ConfigParseException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Parses reward commands from configuration. + */ +final class RewardCommandParser { + + @SuppressWarnings("unchecked") + List parseCommands(Map rewardMap) throws ConfigParseException { + List commands = new ArrayList<>(); + if (!rewardMap.containsKey("commands")) { + return commands; + } + Object commandsObj = rewardMap.get("commands"); + if (!(commandsObj instanceof List)) { + throw new ConfigParseException( + "Invalid 'commands' type: expected list, got " + + (commandsObj != null ? commandsObj.getClass().getSimpleName() : "null") + ); + } + List commandsList = (List) commandsObj; + for (String command : commandsList) { + if (command == null || command.trim().isEmpty()) { + throw new ConfigParseException("Command cannot be null or empty"); + } + commands.add(command); + } + return commands; + } +} diff --git a/src/main/java/dev/loki/lomines/data/reward/RewardMaterialParser.java b/src/main/java/dev/loki/lomines/data/reward/RewardMaterialParser.java new file mode 100644 index 0000000..9850d04 --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/reward/RewardMaterialParser.java @@ -0,0 +1,46 @@ +package dev.loki.lomines.data.reward; + +import dev.loki.lomines.data.config.parser.ConfigParseException; +import dev.loki.lomines.util.ValidationUtils; +import org.bukkit.Material; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Parses reward materials (blocks) from configuration. + */ +final class RewardMaterialParser { + + @SuppressWarnings("unchecked") + List parseMaterials(Map rewardMap) throws ConfigParseException { + if (!rewardMap.containsKey("blocks")) { + throw new ConfigParseException("Missing required 'blocks' field in reward"); + } + Object blocksObj = rewardMap.get("blocks"); + if (!(blocksObj instanceof List)) { + throw new ConfigParseException( + "Invalid 'blocks' type: expected list, got " + + (blocksObj != null ? blocksObj.getClass().getSimpleName() : "null") + ); + } + List blocksList = (List) blocksObj; + List materials = new ArrayList<>(); + for (String blockName : blocksList) { + if (blockName == null || blockName.trim().isEmpty()) { + throw new ConfigParseException("Block name cannot be null or empty"); + } + try { + Material material = ValidationUtils.validateMaterial(blockName); + materials.add(material); + } catch (IllegalArgumentException e) { + throw new ConfigParseException( + "Unknown material in reward blocks: '" + blockName + "'", + e + ); + } + } + return materials; + } +} diff --git a/src/main/java/dev/loki/lomines/data/reward/RewardParser.java b/src/main/java/dev/loki/lomines/data/reward/RewardParser.java index 003fee1..13ef724 100644 --- a/src/main/java/dev/loki/lomines/data/reward/RewardParser.java +++ b/src/main/java/dev/loki/lomines/data/reward/RewardParser.java @@ -1,9 +1,6 @@ package dev.loki.lomines.data.reward; import dev.loki.lomines.data.config.parser.ConfigParseException; -import dev.loki.lomines.util.ValidationUtils; - -import org.bukkit.Material; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.inventory.ItemStack; @@ -18,18 +15,15 @@ import java.util.Map; public final class RewardParser { private final RewardItemParser itemParser; + private final RewardMaterialParser materialParser; + private final RewardCommandParser commandParser; public RewardParser() { this.itemParser = new RewardItemParser(); + this.materialParser = new RewardMaterialParser(); + this.commandParser = new RewardCommandParser(); } - /** - * Parses all rewards from the YAML configuration. - * - * @param yaml the YAML configuration - * @return list of parsed Reward objects - * @throws ConfigParseException if reward configuration is invalid - */ public List parseRewards(YamlConfiguration yaml) throws ConfigParseException { List rewards = new ArrayList<>(); @@ -69,20 +63,13 @@ public final class RewardParser { return rewards; } - /** - * Parses a single reward from a map configuration. - * - * @param rewardMap the reward configuration map - * @return the parsed Reward object - * @throws ConfigParseException if the reward configuration is invalid - */ private Reward parseReward(Map rewardMap) throws ConfigParseException { double chance = parseRewardField(rewardMap, "chance", Double.class); boolean preventDrops = parseRewardField(rewardMap, "prevent-drops", Boolean.class, false); - List materials = parseRewardMaterials(rewardMap); + var materials = materialParser.parseMaterials(rewardMap); List items = parseRewardItems(rewardMap); - List commands = parseRewardCommands(rewardMap); + List commands = commandParser.parseCommands(rewardMap); return new Reward(materials, chance, items, commands, preventDrops); } @@ -137,38 +124,6 @@ public final class RewardParser { } } - private List parseRewardMaterials(Map rewardMap) - throws ConfigParseException { - if (!rewardMap.containsKey("blocks")) { - throw new ConfigParseException("Missing required 'blocks' field in reward"); - } - Object blocksObj = rewardMap.get("blocks"); - if (!(blocksObj instanceof List)) { - throw new ConfigParseException( - "Invalid 'blocks' type: expected list, got " + - (blocksObj != null ? blocksObj.getClass().getSimpleName() : "null") - ); - } - @SuppressWarnings("unchecked") - List blocksList = (List) blocksObj; - List materials = new ArrayList<>(); - for (String blockName : blocksList) { - if (blockName == null || blockName.trim().isEmpty()) { - throw new ConfigParseException("Block name cannot be null or empty"); - } - try { - Material material = ValidationUtils.validateMaterial(blockName); - materials.add(material); - } catch (IllegalArgumentException e) { - throw new ConfigParseException( - "Unknown material in reward blocks: '" + blockName + "'", - e - ); - } - } - return materials; - } - private List parseRewardItems(Map rewardMap) throws ConfigParseException { List items = new ArrayList<>(); @@ -197,28 +152,4 @@ public final class RewardParser { } return items; } - - private List parseRewardCommands(Map rewardMap) - throws ConfigParseException { - List commands = new ArrayList<>(); - if (!rewardMap.containsKey("commands")) { - return commands; - } - Object commandsObj = rewardMap.get("commands"); - if (!(commandsObj instanceof List)) { - throw new ConfigParseException( - "Invalid 'commands' type: expected list, got " + - (commandsObj != null ? commandsObj.getClass().getSimpleName() : "null") - ); - } - @SuppressWarnings("unchecked") - List commandsList = (List) commandsObj; - for (String command : commandsList) { - if (command == null || command.trim().isEmpty()) { - throw new ConfigParseException("Command cannot be null or empty"); - } - commands.add(command); - } - return commands; - } } diff --git a/src/main/java/dev/loki/lomines/gui/GroupCreateGui.java b/src/main/java/dev/loki/lomines/gui/GroupCreateGui.java deleted file mode 100644 index 4569f66..0000000 --- a/src/main/java/dev/loki/lomines/gui/GroupCreateGui.java +++ /dev/null @@ -1,216 +0,0 @@ -package dev.loki.lomines.gui; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mines; -import dev.loki.lomines.wand.GroupWandManager; -import dev.loki.lomines.wand.GroupWandSession; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; - -import java.util.ArrayList; -import java.util.List; - -/** - * 6-row chest: left column = create; center 3×3 = nine sub-mines with shared settings. - */ -public final class GroupCreateGui { - - /** - * Create group (same YAML defaults for every created mine). - */ - public static final int SLOT_CREATE = 0; - /** - * Shows /lm group prefix. - */ - public static final int SLOT_INFO = 8; - private static final int SIZE = 54; - /** - * 3×3 mine slots mapping index 0..8 - */ - private static final int[] MINE_SLOTS = {10, 11, 12, 19, 20, 21, 28, 29, 30}; - - private GroupCreateGui() { - } - - public static void open(LoMinesPlugin plugin, Player player) { - GroupCreateGuiHolder holder = new GroupCreateGuiHolder(player.getUniqueId()); - Inventory inv = Bukkit.createInventory(holder, SIZE, - Component.text("LoMines · группа (9 шахт)", NamedTextColor.DARK_GREEN)); - holder.setInventory(inv); - fill(plugin, player, inv); - player.openInventory(inv); - } - - public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv) { - fill(plugin, player, inv); - } - - private static void fill(LoMinesPlugin plugin, Player player, Inventory inv) { - GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId()); - for (int i = 0; i < SIZE; i++) { - inv.setItem(i, filler()); - } - inv.setItem(SLOT_CREATE, createButton()); - inv.setItem(SLOT_INFO, infoPaper(session)); - for (int i = 0; i < MINE_SLOTS.length; i++) { - inv.setItem(MINE_SLOTS[i], mineSlotItem(session, i)); - } - } - - private static ItemStack filler() { - ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); - ItemMeta meta = pane.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text(" ")); - pane.setItemMeta(meta); - } - return pane; - } - - private static ItemStack createButton() { - ItemStack stack = new ItemStack(Material.LIME_CONCRETE); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("Создать группу", NamedTextColor.GREEN)); - meta.lore(List.of( - Component.text("Создаёт шахты с префиксом_1 … _9", NamedTextColor.GRAY), - Component.text("для слотов, где заданы оба угла", NamedTextColor.GRAY) - )); - stack.setItemMeta(meta); - } - return stack; - } - - private static ItemStack infoPaper(GroupWandSession session) { - ItemStack stack = new ItemStack(Material.PAPER); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - if (session.hasBaseName()) { - meta.displayName(Component.text("Префикс: " + session.getBaseName(), NamedTextColor.AQUA)); - meta.lore(List.of( - Component.text("Файлы: " + session.getBaseName() + "_1.yml … _9.yml", NamedTextColor.GRAY), - Component.text("Настройки одинаковые; правьте один и копируйте при нужде", NamedTextColor.DARK_GRAY) - )); - } else { - meta.displayName(Component.text("Задайте префикс", NamedTextColor.RED)); - meta.lore(List.of(Component.text("/lm group <префикс>", NamedTextColor.GRAY))); - } - stack.setItemMeta(meta); - } - return stack; - } - - private static ItemStack mineSlotItem(GroupWandSession session, int index) { - boolean active = session.getActiveSlot() == index; - boolean ready = session.isSlotReady(index); - Material mat; - NamedTextColor nameColor; - if (ready) { - mat = Material.LIME_STAINED_GLASS_PANE; - nameColor = NamedTextColor.GREEN; - } else if (session.getPos1(index) != null) { - mat = Material.YELLOW_STAINED_GLASS_PANE; - nameColor = NamedTextColor.YELLOW; - } else { - mat = Material.RED_STAINED_GLASS_PANE; - nameColor = NamedTextColor.RED; - } - if (active) { - mat = Material.LIGHT_BLUE_STAINED_GLASS_PANE; - } - ItemStack stack = new ItemStack(mat); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - String label = "Шахта " + (index + 1); - meta.displayName(Component.text(label, nameColor)); - List lore = new ArrayList<>(); - lore.add(Component.text(active ? "Активный слот палочки" : "ПКМ — выбрать активным", NamedTextColor.GRAY)); - lore.add(Component.text( - ready ? "Готово (2 точки)" : (session.getPos1(index) != null ? "Нужна 2-я точка" : "Нужны 2 точки"), - NamedTextColor.DARK_GRAY)); - meta.lore(lore); - stack.setItemMeta(meta); - } - return stack; - } - - public static int mineSlotIndex(int rawSlot) { - for (int i = 0; i < MINE_SLOTS.length; i++) { - if (MINE_SLOTS[i] == rawSlot) { - return i; - } - } - return -1; - } - - /** - * @return true if handled (caller should cancel event) - */ - public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot) { - if (rawSlot < 0 || rawSlot >= SIZE) { - return false; - } - GroupWandManager mgr = plugin.getGroupWandManager(); - GroupWandSession session = mgr.getSession(player.getUniqueId()); - if (rawSlot == SLOT_CREATE) { - runCreate(plugin, player, session); - player.closeInventory(); - return true; - } - int mineIdx = mineSlotIndex(rawSlot); - if (mineIdx >= 0) { - session.setActiveSlot(mineIdx); - Inventory top = player.getOpenInventory().getTopInventory(); - if (top.getHolder() instanceof GroupCreateGuiHolder) { - refresh(plugin, player, top); - } - player.sendMessage(Component.text("Активный слот палочки: " + (mineIdx + 1), NamedTextColor.YELLOW)); - return true; - } - return true; - } - - private static void runCreate(LoMinesPlugin plugin, Player player, GroupWandSession session) { - if (!session.hasBaseName()) { - player.sendMessage(Component.text("Сначала: /lm group <префикс>", NamedTextColor.RED)); - return; - } - String base = session.getBaseName(); - if (!base.matches("^[a-zA-Z0-9_-]+$")) { - player.sendMessage(Component.text("Префикс: только латиница, цифры, _ -", NamedTextColor.RED)); - return; - } - Mines mines = plugin.getMines(); - int created = 0; - int skipped = 0; - for (int i = 0; i < 9; i++) { - if (!session.isSlotReady(i)) { - continue; - } - String mineName = base + "_" + (i + 1); - try { - mines.create(mineName, session.getPos1(i), session.getPos2(i)); - created++; - } catch (IllegalArgumentException e) { - player.sendMessage(Component.text("Пропуск (уже есть): " + mineName, NamedTextColor.GOLD)); - skipped++; - } catch (Exception e) { - player.sendMessage(Component.text("Ошибка " + mineName + ": " + e.getMessage(), NamedTextColor.RED)); - plugin.loLogger().error("Group create failed for " + mineName, e); - } - } - if (created == 0) { - player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом). Пустые слоты пропущены.", NamedTextColor.RED)); - return; - } - player.sendMessage(Component.text( - "Создано шахт: " + created + (skipped > 0 ? " (пропущено существующих: " + skipped + ")" : ""), - NamedTextColor.GREEN)); - } -} diff --git a/src/main/java/dev/loki/lomines/gui/MineEditGui.java b/src/main/java/dev/loki/lomines/gui/MineEditGui.java deleted file mode 100644 index f466978..0000000 --- a/src/main/java/dev/loki/lomines/gui/MineEditGui.java +++ /dev/null @@ -1,326 +0,0 @@ -package dev.loki.lomines.gui; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mine; -import dev.loki.lomines.data.config.MineConfig; -import dev.loki.lomines.data.config.RewardConfig; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; - -import java.util.ArrayList; -import java.util.List; - -/** - * Main GUI for editing a mine. - * Provides navigation to sub-editors for different configuration sections. - * Improved UX with better materials, enhanced lore, and clearer information display. - */ -public final class MineEditGui { - - private static final int SIZE = 54; - private static final String TITLE_PREFIX = "Редактор шахты: "; - - // Slot constants - public static final int SLOT_BACK = 49; - public static final int SLOT_BLOCKS = 10; - public static final int SLOT_REGIONS = 12; - public static final int SLOT_RESET = 14; - public static final int SLOT_REWARDS = 16; - public static final int SLOT_TELEPORT = 28; - public static final int SLOT_UI = 30; - public static final int SLOT_SAVE = 32; - public static final int SLOT_DELETE = 34; - public static final int SLOT_INFO = 40; - - // UI Constants - private static final String SEP = "§8───────────────"; - private static final String EDIT = "§e▸ Нажмите для редактирования"; - private static final String CLOSE = "§c▸ Нажмите чтобы закрыть"; - private static final String SAVE = "§a▸ Нажмите для сохранения"; - private static final String DEL = "§c▸ Нажмите для удаления"; - - private MineEditGui() {} - - public static void open(LoMinesPlugin plugin, Player player, String mineName) { - Mine mine = plugin.getMines().find(mineName).orElse(null); - if (mine == null) { - player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); - return; - } - MineEditGuiHolder holder = new MineEditGuiHolder(player.getUniqueId(), mineName); - Inventory inv = Bukkit.createInventory(holder, SIZE, - Component.text(TITLE_PREFIX + mineName, NamedTextColor.DARK_GREEN)); - holder.setInventory(inv); - fill(plugin, mine, inv); - player.openInventory(inv); - } - - public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv, String mineName) { - Mine mine = plugin.getMines().find(mineName).orElse(null); - if (mine == null) { - player.closeInventory(); - return; - } - fill(plugin, mine, inv); - } - - private static void fill(LoMinesPlugin plugin, Mine mine, Inventory inv) { - MineConfig config = mine.getConfig(); - for (int i = 0; i < SIZE; i++) inv.setItem(i, filler()); - inv.setItem(SLOT_BLOCKS, blocksItem(config)); - inv.setItem(SLOT_REGIONS, regionsItem(config)); - inv.setItem(SLOT_RESET, resetItem(config)); - inv.setItem(SLOT_REWARDS, rewardsItem(config)); - inv.setItem(SLOT_TELEPORT, teleportItem(config)); - inv.setItem(SLOT_UI, uiItem(config)); - inv.setItem(SLOT_SAVE, saveItem()); - inv.setItem(SLOT_DELETE, deleteItem(mine.getName())); - inv.setItem(SLOT_INFO, infoItem(mine)); - inv.setItem(SLOT_BACK, backItem()); - } - - private static ItemStack filler() { - ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); - ItemMeta meta = pane.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text(" ")); - pane.setItemMeta(meta); - } - return pane; - } - - private static String loc(Location l) { - if (l == null || l.getWorld() == null) return "§7не задана"; - return String.format("§7%.0f§8/§7%.0f§8/§7%.0f", l.getX(), l.getY(), l.getZ()); - } - - private static String trunc(String s, int max) { - if (s == null || s.isEmpty()) return "§7нет"; - return s.length() > max ? s.substring(0, max - 3) + "..." : s; - } - - private static ItemStack create(Material mat, String name, String... loreLines) { - ItemStack stack = new ItemStack(mat); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text(name, NamedTextColor.WHITE)); - List lore = new ArrayList<>(); - for (String line : loreLines) { - lore.add(Component.text(line, NamedTextColor.GRAY)); - } - meta.lore(lore); - stack.setItemMeta(meta); - } - return stack; - } - - private static ItemStack blocksItem(MineConfig config) { - var w = config.blocks().weights(); - List lore = new ArrayList<>(); - lore.add(SEP); - lore.add("§7Настройки:"); - lore.add(" §fТипов: §7" + config.blocks().blockCount()); - lore.add(" §fРежим: §7" + config.blocks().fillMode().name()); - lore.add(" §fКастомные: " + (config.blocks().hasCustomBlocks() ? "§aДа" : "§7Нет")); - if (!w.isEmpty()) { - lore.add(""); - lore.add("§7Топ блоков:"); - w.entrySet().stream().sorted((a, b) -> Double.compare(b.getValue(), a.getValue())).limit(3) - .forEach(e -> lore.add(String.format(" §8• §7%s §8(§7%.0f%%§8)", e.getKey().serialize(), e.getValue() * 100))); - } - lore.add(""); - lore.add(EDIT); - return create(Material.DIAMOND_PICKAXE, "§a§lБлоки", lore.toArray(new String[0])); - } - - private static ItemStack regionsItem(MineConfig config) { - return create(Material.COMPASS, "§a§lРегионы", - SEP, - "§7Настройки:", - " §fКоличество: §7" + config.region().regionCount(), - " §fОбщий объём: §7" + config.region().totalVolume() + " §8блоков", - "", - " §fМир: §7" + config.worldName(), - "", - "§8Изменяется через палочку" - ); - } - - private static ItemStack resetItem(MineConfig config) { - List lore = new ArrayList<>(); - lore.add(SEP); - lore.add("§7Настройки:"); - lore.add(" §fИнтервал: §7" + config.reset().intervalDisplay()); - lore.add(" §fТриггер по %: " + (config.reset().isPercentTriggerEnabled() ? "§a" + config.reset().percentTrigger() + "%" : "§7выкл")); - lore.add(" §fКоманд: §7" + config.reset().commands().size()); - lore.add(""); - lore.add(" §fСообщение: " + (config.reset().broadcastMessage().isEmpty() ? "§7нет" : "§aесть")); - if (!config.reset().broadcastMessage().isEmpty()) { - lore.add(" §8" + trunc(config.reset().broadcastMessage(), 25)); - } - lore.add(""); - lore.add(EDIT); - return create(Material.CLOCK, "§a§lСброс", lore.toArray(new String[0])); - } - - private static ItemStack rewardsItem(MineConfig config) { - var entries = config.rewards().entries(); - long items = entries.stream().flatMap(e -> e.items().stream()).count(); - long cmds = entries.stream().filter(e -> !e.commands().isEmpty()).count(); - long blockDrop = entries.stream().filter(RewardConfig.RewardEntry::preventVanillaDrops).count(); - List lore = new ArrayList<>(); - lore.add(SEP); - lore.add("§7Настройки:"); - lore.add(" §fЗаписей: §7" + entries.size()); - if (!entries.isEmpty()) { - lore.add(""); - lore.add("§7Типы наград:"); - lore.add(" §8• §fПредметов: §7" + items); - lore.add(" §8• §fКоманд: §7" + cmds); - if (blockDrop > 0) lore.add(" §8• §cБлокируют дроп: §7" + blockDrop); - } - lore.add(""); - lore.add(EDIT); - return create(Material.GOLD_INGOT, "§a§lНаграды", lore.toArray(new String[0])); - } - - private static ItemStack teleportItem(MineConfig config) { - List lore = new ArrayList<>(); - lore.add(SEP); - lore.add("§7Настройки:"); - lore.add(" §fТелепорт: " + (config.teleport().enabled() ? "§aвкл" : "§7выкл")); - if (config.teleport().enabled()) { - config.teleport().getLocation().ifPresent(loc -> lore.add(" " + loc(loc))); - } - lore.add(""); - lore.add(" §fСпавн (застрявших): " + (config.playerSpawn().enabled() ? "§aвкл" : "§7выкл")); - if (config.playerSpawn().enabled()) { - config.playerSpawn().getLocation().ifPresent(loc -> lore.add(" " + loc(loc))); - } - lore.add(""); - lore.add("§8Команды:"); - lore.add(" §8• §7/lm setteleport §f"); - lore.add(" §8• §7/lm setspawn §f"); - lore.add(" §8• §7/lm clearspawn §f"); - lore.add(""); - lore.add(EDIT); - return create(Material.ENDER_PEARL, "§a§lТелепорт", lore.toArray(new String[0])); - } - - private static ItemStack uiItem(MineConfig config) { - return create(Material.PAINTING, "§a§lИнтерфейс", - SEP, - "§7Настройки:", - " §fAction Bar: " + (config.ui().actionBarEnabled() ? "§aвкл" : "§7выкл"), - " §fРадиус: §7" + (int) config.ui().actionBarRange() + " §8блоков", - "", - "§7Формат ActionBar:", - " §8" + trunc(config.ui().actionBarFormat(), 28), - "", - "§7Формат таймера: §8" + config.ui().timerFormat(), - "", - EDIT - ); - } - - private static ItemStack saveItem() { - return create(Material.LIME_DYE, "§a§lСохранить изменения", - SEP, - "§7Сохраняет конфигурацию шахты", - "§7в файл на диске", - "", - SAVE - ); - } - - private static ItemStack deleteItem(String mineName) { - return create(Material.TNT, "§c§lУдалить шахту", - SEP, - "§c§lВнимание!", - "§cЭто действие нельзя отменить!", - "", - "§7Шахта: §f" + mineName, - "", - DEL - ); - } - - private static ItemStack infoItem(Mine mine) { - int blocks = mine.getBlocks(); - int total = mine.getTotalVolume(); - double percent = mine.getPercentFilled(); - String c = percent > 50 ? "§a" : percent > 25 ? "§e" : "§c"; - int filled = (int) Math.round(percent / 100.0 * 10); - String bar = "§a" + "█".repeat(filled) + "§8" + "░".repeat(10 - filled); - return create(Material.BOOK, "§b§lИнформация", - SEP, - "§7Базовые данные:", - " §fНазвание: §7" + mine.getName(), - " §fМир: §7" + mine.getConfig().worldName(), - "", - "§7Состояние заполнения:", - " §fБлоков: " + c + blocks + "§8/§7" + total, - " §fПроцент: " + c + String.format("%.1f%%", percent), - " " + bar - ); - } - - private static ItemStack backItem() { - return create(Material.ARROW, "§c§lЗакрыть", - SEP, - "§7Закрыть редактор", - "", - CLOSE - ); - } - - public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, String mineName) { - if (rawSlot < 0 || rawSlot >= SIZE) return false; - switch (rawSlot) { - case SLOT_BACK -> { - player.closeInventory(); - return true; - } - case SLOT_BLOCKS -> { - player.sendMessage(Component.text("Открытие редактора блоков... (в разработке)", NamedTextColor.YELLOW)); - return true; - } - case SLOT_RESET -> { - player.sendMessage(Component.text("Открытие редактора сброса... (в разработке)", NamedTextColor.YELLOW)); - return true; - } - case SLOT_REWARDS -> { - player.sendMessage(Component.text("Открытие редактора наград... (в разработке)", NamedTextColor.YELLOW)); - return true; - } - case SLOT_TELEPORT -> { - player.sendMessage(Component.text("Открытие редактора телепорта... (в разработке)", NamedTextColor.YELLOW)); - return true; - } - case SLOT_UI -> { - player.sendMessage(Component.text("Открытие редактора интерфейса... (в разработке)", NamedTextColor.YELLOW)); - return true; - } - case SLOT_SAVE -> { - player.sendMessage(Component.text("Конфигурация сохранена для шахты: " + mineName, NamedTextColor.GREEN)); - player.closeInventory(); - return true; - } - case SLOT_DELETE -> { - ConfirmDeleteGui.open(plugin, player, mineName); - return true; - } - case SLOT_INFO, SLOT_REGIONS -> { - return true; - } - } - return true; - } -} diff --git a/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java b/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java new file mode 100644 index 0000000..352d383 --- /dev/null +++ b/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java @@ -0,0 +1,54 @@ +package dev.loki.lomines.gui.common; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** + * Factory for creating ItemStacks with name and lore. + */ +final class ItemStackFactory { + + private ItemStackFactory() {} + + static ItemStack create(Material mat, String name, String... loreLines) { + ItemStack stack = new ItemStack(mat); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text(name, NamedTextColor.WHITE)); + List lore = new ArrayList<>(); + for (String line : loreLines) { + lore.add(Component.text(line, NamedTextColor.GRAY)); + } + meta.lore(lore); + stack.setItemMeta(meta); + } + return stack; + } + + static String loc(Location l) { + if (l == null || l.getWorld() == null) return "§7не задана"; + return String.format("§7%.0f§8/§7%.0f§8/§7%.0f", l.getX(), l.getY(), l.getZ()); + } + + static String trunc(String s, int max) { + if (s == null || s.isEmpty()) return "§7нет"; + return s.length() > max ? s.substring(0, max - 3) + "..." : s; + } + + static ItemStack filler() { + ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); + ItemMeta meta = pane.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text(" ")); + pane.setItemMeta(meta); + } + return pane; + } +} diff --git a/src/main/java/dev/loki/lomines/gui/package-info.java b/src/main/java/dev/loki/lomines/gui/common/package-info.java similarity index 100% rename from src/main/java/dev/loki/lomines/gui/package-info.java rename to src/main/java/dev/loki/lomines/gui/common/package-info.java diff --git a/src/main/java/dev/loki/lomines/gui/ConfirmDeleteGui.java b/src/main/java/dev/loki/lomines/gui/confirm/ConfirmDeleteGui.java similarity index 99% rename from src/main/java/dev/loki/lomines/gui/ConfirmDeleteGui.java rename to src/main/java/dev/loki/lomines/gui/confirm/ConfirmDeleteGui.java index 82c3701..ab8910e 100644 --- a/src/main/java/dev/loki/lomines/gui/ConfirmDeleteGui.java +++ b/src/main/java/dev/loki/lomines/gui/confirm/ConfirmDeleteGui.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.gui; +package dev.loki.lomines.gui.confirm; import dev.loki.lomines.LoMinesPlugin; import net.kyori.adventure.text.Component; diff --git a/src/main/java/dev/loki/lomines/gui/ConfirmDeleteGuiHolder.java b/src/main/java/dev/loki/lomines/gui/confirm/ConfirmDeleteGuiHolder.java similarity index 95% rename from src/main/java/dev/loki/lomines/gui/ConfirmDeleteGuiHolder.java rename to src/main/java/dev/loki/lomines/gui/confirm/ConfirmDeleteGuiHolder.java index de7bfb4..6a9bcde 100644 --- a/src/main/java/dev/loki/lomines/gui/ConfirmDeleteGuiHolder.java +++ b/src/main/java/dev/loki/lomines/gui/confirm/ConfirmDeleteGuiHolder.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.gui; +package dev.loki.lomines.gui.confirm; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; diff --git a/src/main/java/dev/loki/lomines/gui/group/GroupCreateGui.java b/src/main/java/dev/loki/lomines/gui/group/GroupCreateGui.java new file mode 100644 index 0000000..676a815 --- /dev/null +++ b/src/main/java/dev/loki/lomines/gui/group/GroupCreateGui.java @@ -0,0 +1,123 @@ +package dev.loki.lomines.gui.group; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.Mines; +import dev.loki.lomines.wand.GroupWandManager; +import dev.loki.lomines.wand.GroupWandSession; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +/** + * 6-row chest: left column = create; center 3×3 = nine sub-mines with shared settings. + */ +public final class GroupCreateGui { + + public static final int SLOT_CREATE = 0; + public static final int SLOT_INFO = 8; + private static final int SIZE = 54; + private static final int[] MINE_SLOTS = {10, 11, 12, 19, 20, 21, 28, 29, 30}; + + private GroupCreateGui() {} + + public static void open(LoMinesPlugin plugin, Player player) { + GroupCreateGuiHolder holder = new GroupCreateGuiHolder(player.getUniqueId()); + Inventory inv = Bukkit.createInventory(holder, SIZE, + Component.text("LoMines · группа (9 шахт)", NamedTextColor.DARK_GREEN)); + holder.setInventory(inv); + fill(plugin, player, inv); + player.openInventory(inv); + } + + public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv) { + fill(plugin, player, inv); + } + + private static void fill(LoMinesPlugin plugin, Player player, Inventory inv) { + GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId()); + for (int i = 0; i < SIZE; i++) { + inv.setItem(i, GroupCreateItems.filler()); + } + inv.setItem(SLOT_CREATE, GroupCreateItems.createButton()); + inv.setItem(SLOT_INFO, GroupCreateItems.infoPaper(session)); + for (int i = 0; i < MINE_SLOTS.length; i++) { + inv.setItem(MINE_SLOTS[i], GroupCreateItems.mineSlotItem(session, i)); + } + } + + public static int mineSlotIndex(int rawSlot) { + for (int i = 0; i < MINE_SLOTS.length; i++) { + if (MINE_SLOTS[i] == rawSlot) { + return i; + } + } + return -1; + } + + public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot) { + if (rawSlot < 0 || rawSlot >= SIZE) { + return false; + } + GroupWandManager mgr = plugin.getGroupWandManager(); + GroupWandSession session = mgr.getSession(player.getUniqueId()); + + if (rawSlot == SLOT_CREATE) { + runCreate(plugin, player, session); + player.closeInventory(); + return true; + } + + int mineIdx = mineSlotIndex(rawSlot); + if (mineIdx >= 0) { + session.setActiveSlot(mineIdx); + Inventory top = player.getOpenInventory().getTopInventory(); + if (top.getHolder() instanceof GroupCreateGuiHolder) { + refresh(plugin, player, top); + } + player.sendMessage(Component.text("Активный слот палочки: " + (mineIdx + 1), NamedTextColor.YELLOW)); + return true; + } + return true; + } + + private static void runCreate(LoMinesPlugin plugin, Player player, GroupWandSession session) { + if (!session.hasBaseName()) { + player.sendMessage(Component.text("Сначала: /lm group <префикс>", NamedTextColor.RED)); + return; + } + String base = session.getBaseName(); + if (!base.matches("^[a-zA-Z0-9_-]+$")) { + player.sendMessage(Component.text("Префикс: только латиница, цифры, _ -", NamedTextColor.RED)); + return; + } + + Mines mines = plugin.getMines(); + int created = 0; + int skipped = 0; + + for (int i = 0; i < 9; i++) { + if (!session.isSlotReady(i)) continue; + String mineName = base + "_" + (i + 1); + try { + mines.create(mineName, session.getPos1(i), session.getPos2(i)); + created++; + } catch (IllegalArgumentException e) { + player.sendMessage(Component.text("Пропуск (уже есть): " + mineName, NamedTextColor.GOLD)); + skipped++; + } catch (Exception e) { + player.sendMessage(Component.text("Ошибка " + mineName + ": " + e.getMessage(), NamedTextColor.RED)); + plugin.loLogger().error("Group create failed for " + mineName, e); + } + } + + if (created == 0) { + player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом).", NamedTextColor.RED)); + return; + } + player.sendMessage(Component.text( + "Создано шахт: " + created + (skipped > 0 ? " (пропущено: " + skipped + ")" : ""), + NamedTextColor.GREEN)); + } +} diff --git a/src/main/java/dev/loki/lomines/gui/GroupCreateGuiHolder.java b/src/main/java/dev/loki/lomines/gui/group/GroupCreateGuiHolder.java similarity index 94% rename from src/main/java/dev/loki/lomines/gui/GroupCreateGuiHolder.java rename to src/main/java/dev/loki/lomines/gui/group/GroupCreateGuiHolder.java index 23d410a..8d43c69 100644 --- a/src/main/java/dev/loki/lomines/gui/GroupCreateGuiHolder.java +++ b/src/main/java/dev/loki/lomines/gui/group/GroupCreateGuiHolder.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.gui; +package dev.loki.lomines.gui.group; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; diff --git a/src/main/java/dev/loki/lomines/gui/group/GroupCreateItems.java b/src/main/java/dev/loki/lomines/gui/group/GroupCreateItems.java new file mode 100644 index 0000000..4ac70ef --- /dev/null +++ b/src/main/java/dev/loki/lomines/gui/group/GroupCreateItems.java @@ -0,0 +1,103 @@ +package dev.loki.lomines.gui.group; + +import dev.loki.lomines.wand.GroupWandSession; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** + * Item factory for GroupCreateGui. + */ +final class GroupCreateItems { + + private GroupCreateItems() {} + + static ItemStack filler() { + ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); + ItemMeta meta = pane.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text(" ")); + pane.setItemMeta(meta); + } + return pane; + } + + static ItemStack createButton() { + ItemStack stack = new ItemStack(Material.LIME_CONCRETE); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text("Создать группу", NamedTextColor.GREEN)); + meta.lore(List.of( + Component.text("Создаёт шахты с префиксом_1 … _9", NamedTextColor.GRAY), + Component.text("для слотов, где заданы оба угла", NamedTextColor.GRAY) + )); + stack.setItemMeta(meta); + } + return stack; + } + + static ItemStack infoPaper(GroupWandSession session) { + ItemStack stack = new ItemStack(Material.PAPER); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + if (session.hasBaseName()) { + meta.displayName(Component.text("Префикс: " + session.getBaseName(), NamedTextColor.AQUA)); + meta.lore(List.of( + Component.text("Файлы: " + session.getBaseName() + "_1.yml … _9.yml", NamedTextColor.GRAY), + Component.text("Настройки одинаковые; правьте один и копируйте при нужде", NamedTextColor.DARK_GRAY) + )); + } else { + meta.displayName(Component.text("Задайте префикс", NamedTextColor.RED)); + meta.lore(List.of(Component.text("/lm group <префикс>", NamedTextColor.GRAY))); + } + stack.setItemMeta(meta); + } + return stack; + } + + static ItemStack mineSlotItem(GroupWandSession session, int index) { + boolean active = session.getActiveSlot() == index; + boolean ready = session.isSlotReady(index); + + Material mat = getMaterialForSlot(session, index, ready, active); + NamedTextColor nameColor = ready ? NamedTextColor.GREEN : + (session.getPos1(index) != null ? NamedTextColor.YELLOW : NamedTextColor.RED); + + ItemStack stack = new ItemStack(mat); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + String label = "Шахта " + (index + 1); + meta.displayName(Component.text(label, nameColor)); + meta.lore(buildLore(session, index, active, ready)); + stack.setItemMeta(meta); + } + return stack; + } + + private static Material getMaterialForSlot(GroupWandSession session, int index, boolean ready, boolean active) { + if (active) { + return Material.LIGHT_BLUE_STAINED_GLASS_PANE; + } + if (ready) { + return Material.LIME_STAINED_GLASS_PANE; + } + if (session.getPos1(index) != null) { + return Material.YELLOW_STAINED_GLASS_PANE; + } + return Material.RED_STAINED_GLASS_PANE; + } + + private static List buildLore(GroupWandSession session, int index, boolean active, boolean ready) { + List lore = new ArrayList<>(); + lore.add(Component.text(active ? "Активный слот палочки" : "ПКМ — выбрать активным", NamedTextColor.GRAY)); + lore.add(Component.text( + ready ? "Готово (2 точки)" : (session.getPos1(index) != null ? "Нужна 2-я точка" : "Нужны 2 точки"), + NamedTextColor.DARK_GRAY)); + return lore; + } +} diff --git a/src/main/java/dev/loki/lomines/gui/mine/MineEditGui.java b/src/main/java/dev/loki/lomines/gui/mine/MineEditGui.java new file mode 100644 index 0000000..aefb07d --- /dev/null +++ b/src/main/java/dev/loki/lomines/gui/mine/MineEditGui.java @@ -0,0 +1,118 @@ +package dev.loki.lomines.gui.mine; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.gui.common.ItemStackFactory; +import dev.loki.lomines.gui.confirm.ConfirmDeleteGui; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +/** + * Main GUI for editing a mine. + * Provides navigation to sub-editors for different configuration sections. + * Improved UX with better materials, enhanced lore, and clearer information display. + */ +public final class MineEditGui { + + private static final int SIZE = 54; + private static final String TITLE_PREFIX = "Редактор шахты: "; + + // Slot constants + public static final int SLOT_BACK = 49; + public static final int SLOT_BLOCKS = 10; + public static final int SLOT_REGIONS = 12; + public static final int SLOT_RESET = 14; + public static final int SLOT_REWARDS = 16; + public static final int SLOT_TELEPORT = 28; + public static final int SLOT_UI = 30; + public static final int SLOT_SAVE = 32; + public static final int SLOT_DELETE = 34; + public static final int SLOT_INFO = 40; + + private MineEditGui() {} + + public static void open(LoMinesPlugin plugin, Player player, String mineName) { + Mine mine = plugin.getMines().find(mineName).orElse(null); + if (mine == null) { + player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); + return; + } + MineEditGuiHolder holder = new MineEditGuiHolder(player.getUniqueId(), mineName); + Inventory inv = Bukkit.createInventory(holder, SIZE, + Component.text(TITLE_PREFIX + mineName, NamedTextColor.DARK_GREEN)); + holder.setInventory(inv); + fill(plugin, mine, inv); + player.openInventory(inv); + } + + public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv, String mineName) { + Mine mine = plugin.getMines().find(mineName).orElse(null); + if (mine == null) { + player.closeInventory(); + return; + } + fill(plugin, mine, inv); + } + + private static void fill(LoMinesPlugin plugin, Mine mine, Inventory inv) { + MineConfig config = mine.getConfig(); + for (int i = 0; i < SIZE; i++) inv.setItem(i, MineEditItems.filler()); + inv.setItem(SLOT_BLOCKS, MineEditItems.blocksItem(config)); + inv.setItem(SLOT_REGIONS, MineEditItems.regionsItem(config)); + inv.setItem(SLOT_RESET, MineEditItems.resetItem(config)); + inv.setItem(SLOT_REWARDS, MineEditItems.rewardsItem(config)); + inv.setItem(SLOT_TELEPORT, MineEditItems.teleportItem(config)); + inv.setItem(SLOT_UI, MineEditItems.uiItem(config)); + inv.setItem(SLOT_SAVE, MineEditItems.saveItem()); + inv.setItem(SLOT_DELETE, MineEditItems.deleteItem(mine.getName())); + inv.setItem(SLOT_INFO, MineEditItems.infoItem(mine)); + inv.setItem(SLOT_BACK, MineEditItems.backItem()); + } + + public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, String mineName) { + if (rawSlot < 0 || rawSlot >= SIZE) return false; + switch (rawSlot) { + case SLOT_BACK -> { + player.closeInventory(); + return true; + } + case SLOT_BLOCKS -> { + player.sendMessage(Component.text("Открытие редактора блоков... (в разработке)", NamedTextColor.YELLOW)); + return true; + } + case SLOT_RESET -> { + player.sendMessage(Component.text("Открытие редактора сброса... (в разработке)", NamedTextColor.YELLOW)); + return true; + } + case SLOT_REWARDS -> { + player.sendMessage(Component.text("Открытие редактора наград... (в разработке)", NamedTextColor.YELLOW)); + return true; + } + case SLOT_TELEPORT -> { + player.sendMessage(Component.text("Открытие редактора телепорта... (в разработке)", NamedTextColor.YELLOW)); + return true; + } + case SLOT_UI -> { + player.sendMessage(Component.text("Открытие редактора интерфейса... (в разработке)", NamedTextColor.YELLOW)); + return true; + } + case SLOT_SAVE -> { + player.sendMessage(Component.text("Конфигурация сохранена для шахты: " + mineName, NamedTextColor.GREEN)); + player.closeInventory(); + return true; + } + case SLOT_DELETE -> { + ConfirmDeleteGui.open(plugin, player, mineName); + return true; + } + case SLOT_INFO, SLOT_REGIONS -> { + return true; + } + } + return true; + } +} diff --git a/src/main/java/dev/loki/lomines/gui/MineEditGuiHolder.java b/src/main/java/dev/loki/lomines/gui/mine/MineEditGuiHolder.java similarity index 95% rename from src/main/java/dev/loki/lomines/gui/MineEditGuiHolder.java rename to src/main/java/dev/loki/lomines/gui/mine/MineEditGuiHolder.java index 17b0e88..040aae4 100644 --- a/src/main/java/dev/loki/lomines/gui/MineEditGuiHolder.java +++ b/src/main/java/dev/loki/lomines/gui/mine/MineEditGuiHolder.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.gui; +package dev.loki.lomines.gui.mine; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; diff --git a/src/main/java/dev/loki/lomines/gui/mine/MineEditItems.java b/src/main/java/dev/loki/lomines/gui/mine/MineEditItems.java new file mode 100644 index 0000000..fd2afa9 --- /dev/null +++ b/src/main/java/dev/loki/lomines/gui/mine/MineEditItems.java @@ -0,0 +1,189 @@ +package dev.loki.lomines.gui.mine; + +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.data.config.RewardConfig; +import dev.loki.lomines.gui.common.ItemStackFactory; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +/** + * Item factory for MineEditGui. Creates all inventory items. + */ +final class MineEditItems { + + private static final String SEP = "§8───────────────"; + private static final String EDIT = "§e▸ Нажмите для редактирования"; + private static final String CLOSE = "§c▸ Нажмите чтобы закрыть"; + private static final String SAVE = "§a▸ Нажмите для сохранения"; + private static final String DEL = "§c▸ Нажмите для удаления"; + + private MineEditItems() {} + + static ItemStack filler() { + return ItemStackFactory.filler(); + } + + static ItemStack blocksItem(MineConfig config) { + var w = config.blocks().weights(); + List lore = new ArrayList<>(); + lore.add(SEP); + lore.add("§7Настройки:"); + lore.add(" §fТипов: §7" + config.blocks().blockCount()); + lore.add(" §fРежим: §7" + config.blocks().fillMode().name()); + lore.add(" §fКастомные: " + (config.blocks().hasCustomBlocks() ? "§aДа" : "§7Нет")); + if (!w.isEmpty()) { + lore.add(""); + lore.add("§7Топ блоков:"); + w.entrySet().stream().sorted((a, b) -> Double.compare(b.getValue(), a.getValue())).limit(3) + .forEach(e -> lore.add(String.format(" §8• §7%s §8(§7%.0f%%§8)", e.getKey().serialize(), e.getValue() * 100))); + } + lore.add(""); + lore.add(EDIT); + return ItemStackFactory.create(Material.DIAMOND_PICKAXE, "§a§lБлоки", lore.toArray(new String[0])); + } + + static ItemStack regionsItem(MineConfig config) { + return ItemStackFactory.create(Material.COMPASS, "§a§lРегионы", + SEP, + "§7Настройки:", + " §fКоличество: §7" + config.region().regionCount(), + " §fОбщий объём: §7" + config.region().totalVolume() + " §8блоков", + "", + " §fМир: §7" + config.worldName(), + "", + "§8Изменяется через палочку" + ); + } + + static ItemStack resetItem(MineConfig config) { + List lore = new ArrayList<>(); + lore.add(SEP); + lore.add("§7Настройки:"); + lore.add(" §fИнтервал: §7" + config.reset().intervalDisplay()); + lore.add(" §fТриггер по %: " + (config.reset().isPercentTriggerEnabled() ? "§a" + config.reset().percentTrigger() + "%" : "§7выкл")); + lore.add(" §fКоманд: §7" + config.reset().commands().size()); + lore.add(""); + lore.add(" §fСообщение: " + (config.reset().broadcastMessage().isEmpty() ? "§7нет" : "§aесть")); + if (!config.reset().broadcastMessage().isEmpty()) { + lore.add(" §8" + ItemStackFactory.trunc(config.reset().broadcastMessage(), 25)); + } + lore.add(""); + lore.add(EDIT); + return ItemStackFactory.create(Material.CLOCK, "§a§lСброс", lore.toArray(new String[0])); + } + + static ItemStack rewardsItem(MineConfig config) { + var entries = config.rewards().entries(); + long items = entries.stream().flatMap(e -> e.items().stream()).count(); + long cmds = entries.stream().filter(e -> !e.commands().isEmpty()).count(); + long blockDrop = entries.stream().filter(RewardConfig.RewardEntry::preventVanillaDrops).count(); + List lore = new ArrayList<>(); + lore.add(SEP); + lore.add("§7Настройки:"); + lore.add(" §fЗаписей: §7" + entries.size()); + if (!entries.isEmpty()) { + lore.add(""); + lore.add("§7Типы наград:"); + lore.add(" §8• §fПредметов: §7" + items); + lore.add(" §8• §fКоманд: §7" + cmds); + if (blockDrop > 0) lore.add(" §8• §cБлокируют дроп: §7" + blockDrop); + } + lore.add(""); + lore.add(EDIT); + return ItemStackFactory.create(Material.GOLD_INGOT, "§a§lНаграды", lore.toArray(new String[0])); + } + + static ItemStack teleportItem(MineConfig config) { + List lore = new ArrayList<>(); + lore.add(SEP); + lore.add("§7Настройки:"); + lore.add(" §fТелепорт: " + (config.teleport().enabled() ? "§aвкл" : "§7выкл")); + if (config.teleport().enabled()) { + config.teleport().getLocation().ifPresent(loc -> lore.add(" " + ItemStackFactory.loc(loc))); + } + lore.add(""); + lore.add(" §fСпавн (застрявших): " + (config.playerSpawn().enabled() ? "§aвкл" : "§7выкл")); + if (config.playerSpawn().enabled()) { + config.playerSpawn().getLocation().ifPresent(loc -> lore.add(" " + ItemStackFactory.loc(loc))); + } + lore.add(""); + lore.add("§8Команды:"); + lore.add(" §8• §7/lm setteleport §f"); + lore.add(" §8• §7/lm setspawn §f"); + lore.add(" §8• §7/lm clearspawn §f"); + lore.add(""); + lore.add(EDIT); + return ItemStackFactory.create(Material.ENDER_PEARL, "§a§lТелепорт", lore.toArray(new String[0])); + } + + static ItemStack uiItem(MineConfig config) { + return ItemStackFactory.create(Material.PAINTING, "§a§lИнтерфейс", + SEP, + "§7Настройки:", + " §fAction Bar: " + (config.ui().actionBarEnabled() ? "§aвкл" : "§7выкл"), + " §fРадиус: §7" + (int) config.ui().actionBarRange() + " §8блоков", + "", + "§7Формат ActionBar:", + " §8" + ItemStackFactory.trunc(config.ui().actionBarFormat(), 28), + "", + "§7Формат таймера: §8" + config.ui().timerFormat(), + "", + EDIT + ); + } + + static ItemStack saveItem() { + return ItemStackFactory.create(Material.LIME_DYE, "§a§lСохранить изменения", + SEP, + "§7Сохраняет конфигурацию шахты", + "§7в файл на диске", + "", + SAVE + ); + } + + static ItemStack deleteItem(String mineName) { + return ItemStackFactory.create(Material.TNT, "§c§lУдалить шахту", + SEP, + "§c§lВнимание!", + "§cЭто действие нельзя отменить!", + "", + "§7Шахта: §f" + mineName, + "", + DEL + ); + } + + static ItemStack infoItem(Mine mine) { + int blocks = mine.getBlocks(); + int total = mine.getTotalVolume(); + double percent = mine.getPercentFilled(); + String c = percent > 50 ? "§a" : percent > 25 ? "§e" : "§c"; + int filled = (int) Math.round(percent / 100.0 * 10); + String bar = "§a" + "█".repeat(filled) + "§8" + "░".repeat(10 - filled); + return ItemStackFactory.create(Material.BOOK, "§b§lИнформация", + SEP, + "§7Базовые данные:", + " §fНазвание: §7" + mine.getName(), + " §fМир: §7" + mine.getConfig().worldName(), + "", + "§7Состояние заполнения:", + " §fБлоков: " + c + blocks + "§8/§7" + total, + " §fПроцент: " + c + String.format("%.1f%%", percent), + " " + bar + ); + } + + static ItemStack backItem() { + return ItemStackFactory.create(Material.ARROW, "§c§lЗакрыть", + SEP, + "§7Закрыть редактор", + "", + CLOSE + ); + } +} diff --git a/src/main/java/dev/loki/lomines/handler/ActionBarHandler.java b/src/main/java/dev/loki/lomines/handler/ActionBarHandler.java index 1bee018..61ebb88 100644 --- a/src/main/java/dev/loki/lomines/handler/ActionBarHandler.java +++ b/src/main/java/dev/loki/lomines/handler/ActionBarHandler.java @@ -1,6 +1,6 @@ package dev.loki.lomines.handler; -import dev.loki.lomines.core.Mine; +import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.util.format.TimeFormatter; import dev.loki.lomines.util.location.Cuboid; import dev.lolib.utils.ActionBar; diff --git a/src/main/java/dev/loki/lomines/handler/MineBlockHandler.java b/src/main/java/dev/loki/lomines/handler/MineBlockHandler.java index d613f6e..b643ae5 100644 --- a/src/main/java/dev/loki/lomines/handler/MineBlockHandler.java +++ b/src/main/java/dev/loki/lomines/handler/MineBlockHandler.java @@ -1,7 +1,7 @@ package dev.loki.lomines.handler; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mine; +import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.data.config.MineConfig; import org.bukkit.block.Block; import org.bukkit.entity.Player; diff --git a/src/main/java/dev/loki/lomines/handler/MineResetHandler.java b/src/main/java/dev/loki/lomines/handler/MineResetHandler.java index 76e81d6..ca7c6c4 100644 --- a/src/main/java/dev/loki/lomines/handler/MineResetHandler.java +++ b/src/main/java/dev/loki/lomines/handler/MineResetHandler.java @@ -1,7 +1,7 @@ package dev.loki.lomines.handler; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mine; +import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.data.config.block.FillMode; import dev.loki.lomines.util.block.BlockUpdateUtil; import dev.loki.lomines.util.location.Cuboid; diff --git a/src/main/java/dev/loki/lomines/integration/placeholder/LoMinesPlaceholderExpansion.java b/src/main/java/dev/loki/lomines/integration/placeholder/LoMinesPlaceholderExpansion.java index 7bce85c..7d5b98d 100644 --- a/src/main/java/dev/loki/lomines/integration/placeholder/LoMinesPlaceholderExpansion.java +++ b/src/main/java/dev/loki/lomines/integration/placeholder/LoMinesPlaceholderExpansion.java @@ -1,7 +1,7 @@ package dev.loki.lomines.integration.placeholder; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mine; +import dev.loki.lomines.core.mine.Mine; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardMemberHandler.java b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardMemberHandler.java new file mode 100644 index 0000000..a651b0b --- /dev/null +++ b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardMemberHandler.java @@ -0,0 +1,53 @@ +package dev.loki.lomines.integration.worldguard; + +import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; +import dev.loki.lomines.LoMinesPlugin; + +import java.util.UUID; + +/** + * Handles adding owners and members to WorldGuard regions. + */ +final class WorldGuardMemberHandler { + + private final LoMinesPlugin plugin; + + WorldGuardMemberHandler(LoMinesPlugin plugin) { + this.plugin = plugin; + } + + void applyOwnersAndMembers(ProtectedCuboidRegion region, WorldGuardConfig wgConfig) { + applyOwners(region, wgConfig); + applyMembers(region, wgConfig); + } + + private void applyOwners(ProtectedCuboidRegion region, WorldGuardConfig wgConfig) { + for (String owner : wgConfig.owners()) { + if (owner.startsWith("uuid:")) { + try { + UUID uuid = UUID.fromString(owner.substring(5)); + region.getOwners().addPlayer(uuid); + } catch (IllegalArgumentException e) { + plugin.loLogger().warn("Invalid owner UUID: " + owner); + } + } else { + region.getOwners().addPlayer(owner); + } + } + } + + private void applyMembers(ProtectedCuboidRegion region, WorldGuardConfig wgConfig) { + for (String member : wgConfig.members()) { + if (member.startsWith("uuid:")) { + try { + UUID uuid = UUID.fromString(member.substring(5)); + region.getMembers().addPlayer(uuid); + } catch (IllegalArgumentException e) { + plugin.loLogger().warn("Invalid member UUID: " + member); + } + } else { + region.getMembers().addPlayer(member); + } + } + } +} diff --git a/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardRegionService.java b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardRegionService.java index 1b6bd6e..ca5972b 100644 --- a/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardRegionService.java +++ b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardRegionService.java @@ -11,21 +11,21 @@ import org.bukkit.Bukkit; import org.bukkit.World; import java.util.List; -import java.util.UUID; /** * Service for managing WorldGuard regions for mines. - * Creates, updates, and deletes WorldGuard regions when mines are managed. */ public final class WorldGuardRegionService { private final LoMinesPlugin plugin; private final WorldGuardFlagParser flagParser; + private final WorldGuardMemberHandler memberHandler; private boolean worldGuardEnabled; public WorldGuardRegionService(LoMinesPlugin plugin) { this.plugin = plugin; this.flagParser = new WorldGuardFlagParser(plugin); + this.memberHandler = new WorldGuardMemberHandler(plugin); checkWorldGuard(); } @@ -34,30 +34,21 @@ public final class WorldGuardRegionService { && Bukkit.getPluginManager().isPluginEnabled("WorldGuard"); if (worldGuardEnabled) { - plugin.loLogger().info("WorldGuard integration enabled - auto-region creation active"); + plugin.loLogger().info("WorldGuard integration enabled"); } } - /** - * Creates a WorldGuard region for a mine if enabled in config. - */ public String createRegion(String mineName, MineConfig config) { - if (!worldGuardEnabled) { - return null; - } + if (!worldGuardEnabled) return null; WorldGuardConfig wgConfig = config.worldGuard(); - if (wgConfig == null || !wgConfig.enabled()) { - return null; - } + if (wgConfig == null || !wgConfig.enabled()) return null; String regionName = wgConfig.generateRegionName(mineName); try { RegionManager regionManager = getRegionManager(config.worldName()); - if (regionManager == null) { - return null; - } + if (regionManager == null) return null; if (regionManager.hasRegion(regionName)) { plugin.loLogger().info("WorldGuard region already exists: " + regionName); @@ -70,7 +61,7 @@ public final class WorldGuardRegionService { return null; } - applyOwnersAndMembers(region, wgConfig); + memberHandler.applyOwnersAndMembers(region, wgConfig); applyFlags(region, wgConfig.flags()); regionManager.addRegion(region); @@ -79,31 +70,21 @@ public final class WorldGuardRegionService { } catch (Exception e) { plugin.loLogger().error("Failed to create WorldGuard region for mine " + mineName + ": " + e.getMessage()); - e.printStackTrace(); return null; } } - /** - * Updates an existing WorldGuard region when mine regions change. - */ public boolean updateRegion(String mineName, MineConfig config) { - if (!worldGuardEnabled) { - return false; - } + if (!worldGuardEnabled) return false; WorldGuardConfig wgConfig = config.worldGuard(); - if (wgConfig == null || !wgConfig.enabled()) { - return false; - } + if (wgConfig == null || !wgConfig.enabled()) return false; String regionName = wgConfig.generateRegionName(mineName); try { RegionManager regionManager = getRegionManager(config.worldName()); - if (regionManager == null) { - return false; - } + if (regionManager == null) return false; if (regionManager.hasRegion(regionName)) { regionManager.removeRegion(regionName); @@ -117,26 +98,17 @@ public final class WorldGuardRegionService { } } - /** - * Deletes a WorldGuard region for a mine. - */ public boolean deleteRegion(String mineName, MineConfig config) { - if (!worldGuardEnabled) { - return false; - } + if (!worldGuardEnabled) return false; WorldGuardConfig wgConfig = config.worldGuard(); - if (wgConfig == null) { - return false; - } + if (wgConfig == null) return false; String regionName = wgConfig.generateRegionName(mineName); try { RegionManager regionManager = getRegionManager(config.worldName()); - if (regionManager == null) { - return false; - } + if (regionManager == null) return false; if (regionManager.hasRegion(regionName)) { regionManager.removeRegion(regionName); @@ -153,9 +125,7 @@ public final class WorldGuardRegionService { private RegionManager getRegionManager(String worldName) { World world = Bukkit.getWorld(worldName); - if (world == null) { - return null; - } + if (world == null) return null; return WorldGuard.getInstance() .getPlatform() @@ -165,9 +135,7 @@ public final class WorldGuardRegionService { private ProtectedCuboidRegion createRegionFromCuboids(String regionName, MineConfig config) { List regions = config.region().regions(); - if (regions.isEmpty()) { - return null; - } + if (regions.isEmpty()) return null; Cuboid first = regions.get(0); BlockVector3 min = BlockVector3.at(first.getMinX(), first.getMinY(), first.getMinZ()); @@ -183,36 +151,6 @@ public final class WorldGuardRegionService { return mainRegion; } - private void applyOwnersAndMembers(ProtectedCuboidRegion region, WorldGuardConfig wgConfig) { - // Set owners - for (String owner : wgConfig.owners()) { - if (owner.startsWith("uuid:")) { - try { - UUID uuid = UUID.fromString(owner.substring(5)); - region.getOwners().addPlayer(uuid); - } catch (IllegalArgumentException e) { - plugin.loLogger().warn("Invalid owner UUID: " + owner); - } - } else { - region.getOwners().addPlayer(owner); - } - } - - // Set members - for (String member : wgConfig.members()) { - if (member.startsWith("uuid:")) { - try { - UUID uuid = UUID.fromString(member.substring(5)); - region.getMembers().addPlayer(uuid); - } catch (IllegalArgumentException e) { - plugin.loLogger().warn("Invalid member UUID: " + member); - } - } else { - region.getMembers().addPlayer(member); - } - } - } - private void applyFlags(ProtectedCuboidRegion region, List flags) { for (String flagEntry : flags) { flagParser.parseAndSetFlag(region, flagEntry); diff --git a/src/main/java/dev/loki/lomines/listener/BlockBreakListener.java b/src/main/java/dev/loki/lomines/listener/BlockBreakListener.java index 191a111..fd3b312 100644 --- a/src/main/java/dev/loki/lomines/listener/BlockBreakListener.java +++ b/src/main/java/dev/loki/lomines/listener/BlockBreakListener.java @@ -1,7 +1,7 @@ package dev.loki.lomines.listener; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mine; +import dev.loki.lomines.core.mine.Mine; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; diff --git a/src/main/java/dev/loki/lomines/listener/GroupGuiListener.java b/src/main/java/dev/loki/lomines/listener/GroupGuiListener.java index 673832f..ca94f44 100644 --- a/src/main/java/dev/loki/lomines/listener/GroupGuiListener.java +++ b/src/main/java/dev/loki/lomines/listener/GroupGuiListener.java @@ -1,8 +1,8 @@ package dev.loki.lomines.listener; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.gui.GroupCreateGui; -import dev.loki.lomines.gui.GroupCreateGuiHolder; +import dev.loki.lomines.gui.group.GroupCreateGui; +import dev.loki.lomines.gui.group.GroupCreateGuiHolder; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; diff --git a/src/main/java/dev/loki/lomines/listener/MineEditGuiListener.java b/src/main/java/dev/loki/lomines/listener/MineEditGuiListener.java index fd7bbf1..adc9da0 100644 --- a/src/main/java/dev/loki/lomines/listener/MineEditGuiListener.java +++ b/src/main/java/dev/loki/lomines/listener/MineEditGuiListener.java @@ -1,10 +1,10 @@ package dev.loki.lomines.listener; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.gui.ConfirmDeleteGui; -import dev.loki.lomines.gui.ConfirmDeleteGuiHolder; -import dev.loki.lomines.gui.MineEditGui; -import dev.loki.lomines.gui.MineEditGuiHolder; +import dev.loki.lomines.gui.confirm.ConfirmDeleteGui; +import dev.loki.lomines.gui.confirm.ConfirmDeleteGuiHolder; +import dev.loki.lomines.gui.mine.MineEditGui; +import dev.loki.lomines.gui.mine.MineEditGuiHolder; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; diff --git a/src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java b/src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java index a785375..aa7c69c 100644 --- a/src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java +++ b/src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java @@ -1,7 +1,7 @@ package dev.loki.lomines.listener; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.gui.GroupCreateGui; +import dev.loki.lomines.gui.group.GroupCreateGui; import dev.loki.lomines.wand.GroupWandItem; import dev.loki.lomines.wand.GroupWandSession; import net.kyori.adventure.text.Component; diff --git a/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java b/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java index 503801c..2c4125a 100644 --- a/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java +++ b/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java @@ -17,8 +17,7 @@ import java.util.Collection; */ public final class BlockUpdateUtil { - private BlockUpdateUtil() { - } + private BlockUpdateUtil() {} /** * Sends a block update to all players who can see the specified chunk. @@ -27,15 +26,11 @@ public final class BlockUpdateUtil { * @param block the block to update */ public static void sendBlockUpdate(Block block) { - if (block == null) { - return; - } + if (block == null) return; - // Get chunk and players in it Chunk chunk = block.getChunk(); World world = block.getWorld(); - // Send block change to all players in the world who can see this chunk for (Player player : world.getPlayers()) { if (isChunkVisibleToPlayer(player, chunk)) { player.sendBlockChange(block.getLocation(), block.getBlockData()); @@ -63,7 +58,6 @@ public final class BlockUpdateUtil { for (int y = minY; y <= maxY; y++) { for (int z = minZ; z <= maxZ; z++) { Block block = world.getBlockAt(x, y, z); - for (Player player : players) { if (isLocationVisibleToPlayer(player, block.getLocation())) { player.sendBlockChange(block.getLocation(), block.getBlockData()); @@ -81,16 +75,11 @@ public final class BlockUpdateUtil { * @param locations the locations to update */ public static void sendLocationsUpdate(java.util.List locations) { - if (locations == null || locations.isEmpty()) { - return; - } + if (locations == null || locations.isEmpty()) return; for (Location loc : locations) { - if (loc.getWorld() == null) { - continue; - } + if (loc.getWorld() == null) continue; Block block = loc.getBlock(); - for (Player player : loc.getWorld().getPlayers()) { if (isLocationVisibleToPlayer(player, loc)) { player.sendBlockChange(loc, block.getBlockData()); @@ -127,109 +116,8 @@ public final class BlockUpdateUtil { } } - /** - * Finds a safe teleport location near the given destination. - * Checks for suffocation hazards (blocks at head/body level). - *

    Will not teleport too high - limited to maxUpOffset blocks above original.

    - * - * @param destination the desired destination - * @param maxUpOffset maximum blocks to search upward (to avoid teleporting too high) - * @return a safe location (may be the same as destination if safe) - */ - public static Location findSafeTeleportLocation(Location destination, int maxUpOffset) { - if (destination == null || destination.getWorld() == null) { - return destination; - } - - World world = destination.getWorld(); - int x = destination.getBlockX(); - int y = destination.getBlockY(); - int z = destination.getBlockZ(); - float yaw = destination.getYaw(); - float pitch = destination.getPitch(); - - // Clamp maxUpOffset to reasonable range (2-10) - maxUpOffset = Math.max(2, Math.min(maxUpOffset, 10)); - - // Check if original location is safe - if (isSafeLocation(world, x, y, z)) { - return destination; - } - - // Search nearby blocks first (same Y level, closer distance) - int[][] nearby = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - for (int[] offset : nearby) { - if (isSafeLocation(world, x + offset[0], y, z + offset[1])) { - return new Location(world, x + offset[0] + 0.5, y, z + offset[1] + 0.5, yaw, pitch); - } - } - - // Search upward (limited by maxUpOffset to avoid teleporting too high) - for (int offset = 1; offset <= maxUpOffset; offset++) { - if (isSafeLocation(world, x, y + offset, z)) { - return new Location(world, x + 0.5, y + offset, z + 0.5, yaw, pitch); - } - } - - // Search downward - for (int offset = 1; offset <= 5 && y - offset >= world.getMinHeight(); offset++) { - if (isSafeLocation(world, x, y - offset, z)) { - return new Location(world, x + 0.5, y - offset, z + 0.5, yaw, pitch); - } - } - - // Search nearby with small Y offset - int[][] nearbyDiagonal = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}}; - for (int[] offset : nearbyDiagonal) { - for (int yOffset = -1; yOffset <= maxUpOffset; yOffset++) { - int newY = y + yOffset; - if (newY < world.getMinHeight() || newY >= world.getMaxHeight()) { - continue; - } - if (isSafeLocation(world, x + offset[0], newY, z + offset[1])) { - return new Location(world, x + offset[0] + 0.5, newY, z + offset[1] + 0.5, yaw, pitch); - } - } - } - - // If nothing else works, return original but at least center it - return new Location(world, x + 0.5, y, z + 0.5, yaw, pitch); - } - - /** - * Finds a safe teleport location with default max up offset of 3 blocks. - */ - public static Location findSafeTeleportLocation(Location destination) { - return findSafeTeleportLocation(destination, 3); - } - - /** - * Checks if a location is safe for teleport (no suffocation). - */ - private static boolean isSafeLocation(World world, int x, int y, int z) { - // Check feet position (can be passable) - Block feetBlock = world.getBlockAt(x, y, z); - // Check head position (must be passable) - Block headBlock = world.getBlockAt(x, y + 1, z); - - // Safe if feet is passable and head is passable - return isPassable(feetBlock) && isPassable(headBlock); - } - - /** - * Checks if a block is passable (not solid). - */ - private static boolean isPassable(Block block) { - return !block.getType().isSolid() || block.getType().isAir(); - } - - /** - * Checks if a player can see a chunk (is within render distance). - */ private static boolean isChunkVisibleToPlayer(Player player, Chunk chunk) { - if (!player.getWorld().equals(chunk.getWorld())) { - return false; - } + if (!player.getWorld().equals(chunk.getWorld())) return false; int renderDistance = player.getClientViewDistance(); int playerChunkX = player.getLocation().getBlockX() >> 4; @@ -238,19 +126,13 @@ public final class BlockUpdateUtil { int dx = Math.abs(playerChunkX - chunk.getX()); int dz = Math.abs(playerChunkZ - chunk.getZ()); - // Check if chunk is within player's view distance return dx <= renderDistance && dz <= renderDistance; } - /** - * Checks if a location is visible to a player (within render distance). - */ private static boolean isLocationVisibleToPlayer(Player player, Location loc) { - if (!player.getWorld().equals(loc.getWorld())) { - return false; - } + if (!player.getWorld().equals(loc.getWorld())) return false; - double renderDistance = player.getClientViewDistance() * 16; // blocks + double renderDistance = player.getClientViewDistance() * 16; return player.getLocation().distanceSquared(loc) <= renderDistance * renderDistance; } } diff --git a/src/main/java/dev/loki/lomines/util/location/LocationParser.java b/src/main/java/dev/loki/lomines/util/location/LocationParser.java index dc93b01..dd6711b 100644 --- a/src/main/java/dev/loki/lomines/util/location/LocationParser.java +++ b/src/main/java/dev/loki/lomines/util/location/LocationParser.java @@ -67,6 +67,33 @@ public final class LocationParser { } } + /** + * Parses a simple location string in the format "world;x;y;z" without yaw/pitch. + * Returns null if parsing fails. + * + * @param locationString the location string to parse + * @return the parsed Location object, or null if parsing fails + */ + public static Location parseSimple(String locationString) { + if (locationString == null || locationString.isEmpty()) { + return null; + } + String[] parts = locationString.split(";"); + if (parts.length < 4) return null; + + World world = Bukkit.getWorld(parts[0]); + if (world == null) return null; + + try { + double x = Double.parseDouble(parts[1]); + double y = Double.parseDouble(parts[2]); + double z = Double.parseDouble(parts[3]); + return new Location(world, x, y, z); + } catch (NumberFormatException e) { + return null; + } + } + /** * Formats a Location object to a string in the format "world;x;y;z;yaw;pitch". * diff --git a/src/main/java/dev/loki/lomines/util/location/SafeTeleportUtil.java b/src/main/java/dev/loki/lomines/util/location/SafeTeleportUtil.java new file mode 100644 index 0000000..232d843 --- /dev/null +++ b/src/main/java/dev/loki/lomines/util/location/SafeTeleportUtil.java @@ -0,0 +1,117 @@ +package dev.loki.lomines.util.location; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; + +/** + * Utility for finding safe teleport locations. + * Checks for suffocation hazards and finds alternative safe positions. + */ +public final class SafeTeleportUtil { + + private SafeTeleportUtil() {} + + /** + * Finds a safe teleport location near the given destination. + * Checks for suffocation hazards (blocks at head/body level). + *

    Will not teleport too high - limited to maxUpOffset blocks above original.

    + * + * @param destination the desired destination + * @param maxUpOffset maximum blocks to search upward (to avoid teleporting too high) + * @return a safe location (may be the same as destination if safe) + */ + public static Location findSafeTeleportLocation(Location destination, int maxUpOffset) { + if (destination == null || destination.getWorld() == null) { + return destination; + } + + World world = destination.getWorld(); + int x = destination.getBlockX(); + int y = destination.getBlockY(); + int z = destination.getBlockZ(); + float yaw = destination.getYaw(); + float pitch = destination.getPitch(); + + maxUpOffset = Math.max(2, Math.min(maxUpOffset, 10)); + + if (isSafeLocation(world, x, y, z)) { + return destination; + } + + Location result = findNearbySafe(world, x, y, z, yaw, pitch); + if (result != null) return result; + + result = searchUpward(world, x, y, z, yaw, pitch, maxUpOffset); + if (result != null) return result; + + result = searchDownward(world, x, y, z, yaw, pitch); + if (result != null) return result; + + result = searchDiagonal(world, x, y, z, yaw, pitch, maxUpOffset); + if (result != null) return result; + + return new Location(world, x + 0.5, y, z + 0.5, yaw, pitch); + } + + /** + * Finds a safe teleport location with default max up offset of 3 blocks. + */ + public static Location findSafeTeleportLocation(Location destination) { + return findSafeTeleportLocation(destination, 3); + } + + private static Location findNearbySafe(World world, int x, int y, int z, float yaw, float pitch) { + int[][] nearby = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (int[] offset : nearby) { + if (isSafeLocation(world, x + offset[0], y, z + offset[1])) { + return new Location(world, x + offset[0] + 0.5, y, z + offset[1] + 0.5, yaw, pitch); + } + } + return null; + } + + private static Location searchUpward(World world, int x, int y, int z, float yaw, float pitch, int maxUpOffset) { + for (int offset = 1; offset <= maxUpOffset; offset++) { + if (isSafeLocation(world, x, y + offset, z)) { + return new Location(world, x + 0.5, y + offset, z + 0.5, yaw, pitch); + } + } + return null; + } + + private static Location searchDownward(World world, int x, int y, int z, float yaw, float pitch) { + for (int offset = 1; offset <= 5 && y - offset >= world.getMinHeight(); offset++) { + if (isSafeLocation(world, x, y - offset, z)) { + return new Location(world, x + 0.5, y - offset, z + 0.5, yaw, pitch); + } + } + return null; + } + + private static Location searchDiagonal(World world, int x, int y, int z, float yaw, float pitch, int maxUpOffset) { + int[][] nearbyDiagonal = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}}; + for (int[] offset : nearbyDiagonal) { + for (int yOffset = -1; yOffset <= maxUpOffset; yOffset++) { + int newY = y + yOffset; + if (newY < world.getMinHeight() || newY >= world.getMaxHeight()) { + continue; + } + if (isSafeLocation(world, x + offset[0], newY, z + offset[1])) { + return new Location(world, x + offset[0] + 0.5, newY, z + offset[1] + 0.5, yaw, pitch); + } + } + } + return null; + } + + private static boolean isSafeLocation(World world, int x, int y, int z) { + Block feetBlock = world.getBlockAt(x, y, z); + Block headBlock = world.getBlockAt(x, y + 1, z); + return isPassable(feetBlock) && isPassable(headBlock); + } + + private static boolean isPassable(Block block) { + return !block.getType().isSolid() || block.getType().isAir(); + } +} diff --git a/src/main/java/dev/loki/lomines/wand/ParticleUtil.java b/src/main/java/dev/loki/lomines/wand/ParticleUtil.java new file mode 100644 index 0000000..56c11b5 --- /dev/null +++ b/src/main/java/dev/loki/lomines/wand/ParticleUtil.java @@ -0,0 +1,120 @@ +package dev.loki.lomines.wand; + +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.World; +import org.bukkit.entity.Player; + +/** + * Utility class for spawning particles to visualize wand selections. + */ +final class ParticleUtil { + + private ParticleUtil() {} + + static void showCornerParticles(Player player, Location loc, Color color) { + World world = loc.getWorld(); + if (world == null) return; + + Particle.DustOptions dustOptions = new Particle.DustOptions(color, 1.0f); + + for (int i = 0; i < 8; i++) { + double angle = i * Math.PI / 4; + double x = loc.getX() + 0.5 + Math.cos(angle) * 0.5; + double z = loc.getZ() + 0.5 + Math.sin(angle) * 0.5; + double y = loc.getY() + 0.5; + + Location particleLoc = new Location(world, x, y, z); + player.spawnParticle(Particle.DUST, particleLoc, 1, dustOptions); + } + } + + static void showEdgeParticles(Player player, Location pos1, Location pos2, Color color) { + World world = pos1.getWorld(); + if (world == null) return; + + Particle.DustOptions dustOptions = new Particle.DustOptions(color, 0.8f); + + int minX = Math.min(pos1.getBlockX(), pos2.getBlockX()); + int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX()); + int minY = Math.min(pos1.getBlockY(), pos2.getBlockY()); + int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY()); + int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ()); + int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ()); + + double step = 1.0; + + // Bottom face edges + spawnEdgeParticles(player, world, minX, minY, minZ, maxX, minY, minZ, step, dustOptions); + spawnEdgeParticles(player, world, minX, minY, maxZ, maxX, minY, maxZ, step, dustOptions); + spawnEdgeParticles(player, world, minX, minY, minZ, minX, minY, maxZ, step, dustOptions); + spawnEdgeParticles(player, world, maxX, minY, minZ, maxX, minY, maxZ, step, dustOptions); + + // Top face edges + spawnEdgeParticles(player, world, minX, maxY, minZ, maxX, maxY, minZ, step, dustOptions); + spawnEdgeParticles(player, world, minX, maxY, maxZ, maxX, maxY, maxZ, step, dustOptions); + spawnEdgeParticles(player, world, minX, maxY, minZ, minX, maxY, maxZ, step, dustOptions); + spawnEdgeParticles(player, world, maxX, maxY, minZ, maxX, maxY, maxZ, step, dustOptions); + + // Vertical edges + spawnEdgeParticles(player, world, minX, minY, minZ, minX, maxY, minZ, step, dustOptions); + spawnEdgeParticles(player, world, maxX, minY, minZ, maxX, maxY, minZ, step, dustOptions); + spawnEdgeParticles(player, world, minX, minY, maxZ, minX, maxY, maxZ, step, dustOptions); + spawnEdgeParticles(player, world, maxX, minY, maxZ, maxX, maxY, maxZ, step, dustOptions); + } + + private static void spawnEdgeParticles(Player player, World world, + double x1, double y1, double z1, + double x2, double y2, double z2, + double step, Particle.DustOptions dustOptions) { + double distance = Math.sqrt( + Math.pow(x2 - x1, 2) + + Math.pow(y2 - y1, 2) + + Math.pow(z2 - z1, 2) + ); + + int count = (int) (distance / step) + 1; + + for (int i = 0; i <= count; i++) { + double t = (double) i / count; + double x = x1 + (x2 - x1) * t + 0.5; + double y = y1 + (y2 - y1) * t + 0.5; + double z = z1 + (z2 - z1) * t + 0.5; + + Location loc = new Location(world, x, y, z); + if (loc.getWorld() != null && loc.getWorld().equals(player.getWorld())) { + player.spawnParticle(Particle.DUST, loc, 1, dustOptions); + } + } + } + + static void showOutlineParticles(Player player, Location pos1, Location pos2, Color color, int density) { + World world = pos1.getWorld(); + if (world == null || !world.equals(player.getWorld())) return; + + Particle.DustOptions dustOptions = new Particle.DustOptions(color, 0.5f); + + int minX = Math.min(pos1.getBlockX(), pos2.getBlockX()); + int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX()); + int minY = Math.min(pos1.getBlockY(), pos2.getBlockY()); + int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY()); + int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ()); + int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ()); + + Location[] corners = { + new Location(world, minX + 0.5, minY + 0.5, minZ + 0.5), + new Location(world, maxX + 0.5, minY + 0.5, minZ + 0.5), + new Location(world, minX + 0.5, minY + 0.5, maxZ + 0.5), + new Location(world, maxX + 0.5, minY + 0.5, maxZ + 0.5), + new Location(world, minX + 0.5, maxY + 0.5, minZ + 0.5), + new Location(world, maxX + 0.5, maxY + 0.5, minZ + 0.5), + new Location(world, minX + 0.5, maxY + 0.5, maxZ + 0.5), + new Location(world, maxX + 0.5, maxY + 0.5, maxZ + 0.5) + }; + + for (Location corner : corners) { + player.spawnParticle(Particle.DUST, corner, 1, dustOptions); + } + } +} diff --git a/src/main/java/dev/loki/lomines/wand/WandParticleService.java b/src/main/java/dev/loki/lomines/wand/WandParticleService.java index 902b582..9ca1c32 100644 --- a/src/main/java/dev/loki/lomines/wand/WandParticleService.java +++ b/src/main/java/dev/loki/lomines/wand/WandParticleService.java @@ -3,8 +3,6 @@ package dev.loki.lomines.wand; import dev.loki.lomines.LoMinesPlugin; import org.bukkit.Color; import org.bukkit.Location; -import org.bukkit.Particle; -import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; @@ -21,23 +19,17 @@ public final class WandParticleService { private final LoMinesPlugin plugin; private final Map activeTasks = new ConcurrentHashMap<>(); - // Particle colors for different states - private static final Color COLOR_POS1 = Color.LIME; // Green for corner 1 - private static final Color COLOR_POS2 = Color.AQUA; // Cyan for corner 2 - private static final Color COLOR_EDGE = Color.YELLOW; // Yellow for edges - private static final Color COLOR_READY = Color.GREEN; // Green when both corners set + private static final Color COLOR_POS1 = Color.LIME; + private static final Color COLOR_POS2 = Color.AQUA; + private static final Color COLOR_EDGE = Color.YELLOW; + private static final Color COLOR_READY = Color.GREEN; public WandParticleService(LoMinesPlugin plugin) { this.plugin = plugin; } - /** - * Starts showing particles for a player holding the wand. - */ public void startShowingParticles(Player player) { UUID playerId = player.getUniqueId(); - - // Stop existing task if any stopShowingParticles(playerId); BukkitRunnable task = new BukkitRunnable() { @@ -48,28 +40,19 @@ public final class WandParticleService { activeTasks.remove(playerId); return; } - showParticles(player); } }; - task.runTaskTimer(plugin, 0L, 10L); // Every 10 ticks (0.5 seconds) + task.runTaskTimer(plugin, 0L, 10L); activeTasks.put(playerId, task); } - /** - * Stops showing particles for a player. - */ public void stopShowingParticles(UUID playerId) { BukkitRunnable task = activeTasks.remove(playerId); - if (task != null) { - task.cancel(); - } + if (task != null) task.cancel(); } - /** - * Stops all particle tasks. - */ public void stopAll() { for (BukkitRunnable task : activeTasks.values()) { task.cancel(); @@ -88,141 +71,34 @@ public final class WandParticleService { Location pos1 = session.getPos1(activeSlot); Location pos2 = session.getPos2(activeSlot); - // Show particles for corner 1 if set if (pos1 != null && pos1.getWorld() != null) { - showCornerParticles(player, pos1, COLOR_POS1); + ParticleUtil.showCornerParticles(player, pos1, COLOR_POS1); } - // Show particles for corner 2 if set if (pos2 != null && pos2.getWorld() != null) { - showCornerParticles(player, pos2, COLOR_POS2); + ParticleUtil.showCornerParticles(player, pos2, COLOR_POS2); } - // Show edge particles if both corners are set if (pos1 != null && pos2 != null && pos1.getWorld() != null && pos2.getWorld() != null) { if (pos1.getWorld().equals(pos2.getWorld())) { - showEdgeParticles(player, pos1, pos2, session.isSlotReady(activeSlot) ? COLOR_READY : COLOR_EDGE); + Color edgeColor = session.isSlotReady(activeSlot) ? COLOR_READY : COLOR_EDGE; + ParticleUtil.showEdgeParticles(player, pos1, pos2, edgeColor); } } - // Also show other slots that are ready (with lower intensity) + showOtherSlots(player, session, activeSlot); + } + + private void showOtherSlots(Player player, GroupWandSession session, int activeSlot) { for (int i = 0; i < 9; i++) { if (i == activeSlot) continue; if (session.isSlotReady(i)) { Location p1 = session.getPos1(i); Location p2 = session.getPos2(i); if (p1 != null && p2 != null && p1.getWorld() != null && p1.getWorld().equals(player.getWorld())) { - showOutlineParticles(player, p1, p2, Color.GRAY, 5); + ParticleUtil.showOutlineParticles(player, p1, p2, Color.GRAY, 5); } } } } - - private void showCornerParticles(Player player, Location loc, Color color) { - World world = loc.getWorld(); - if (world == null) return; - - Particle.DustOptions dustOptions = new Particle.DustOptions(color, 1.0f); - - // Show particles in a small sphere around the corner - for (int i = 0; i < 8; i++) { - double angle = i * Math.PI / 4; - double x = loc.getX() + 0.5 + Math.cos(angle) * 0.5; - double z = loc.getZ() + 0.5 + Math.sin(angle) * 0.5; - double y = loc.getY() + 0.5; - - Location particleLoc = new Location(world, x, y, z); - player.spawnParticle(Particle.DUST, particleLoc, 1, dustOptions); - } - } - - private void showEdgeParticles(Player player, Location pos1, Location pos2, Color color) { - World world = pos1.getWorld(); - if (world == null) return; - - Particle.DustOptions dustOptions = new Particle.DustOptions(color, 0.8f); - - int minX = Math.min(pos1.getBlockX(), pos2.getBlockX()); - int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX()); - int minY = Math.min(pos1.getBlockY(), pos2.getBlockY()); - int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY()); - int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ()); - int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ()); - - // Show particles along the edges of the cuboid - double step = 1.0; // Every block - - // Bottom face edges - spawnEdgeParticles(player, world, minX, minY, minZ, maxX, minY, minZ, step, dustOptions); // Bottom front - spawnEdgeParticles(player, world, minX, minY, maxZ, maxX, minY, maxZ, step, dustOptions); // Bottom back - spawnEdgeParticles(player, world, minX, minY, minZ, minX, minY, maxZ, step, dustOptions); // Bottom left - spawnEdgeParticles(player, world, maxX, minY, minZ, maxX, minY, maxZ, step, dustOptions); // Bottom right - - // Top face edges - spawnEdgeParticles(player, world, minX, maxY, minZ, maxX, maxY, minZ, step, dustOptions); // Top front - spawnEdgeParticles(player, world, minX, maxY, maxZ, maxX, maxY, maxZ, step, dustOptions); // Top back - spawnEdgeParticles(player, world, minX, maxY, minZ, minX, maxY, maxZ, step, dustOptions); // Top left - spawnEdgeParticles(player, world, maxX, maxY, minZ, maxX, maxY, maxZ, step, dustOptions); // Top right - - // Vertical edges - spawnEdgeParticles(player, world, minX, minY, minZ, minX, maxY, minZ, step, dustOptions); // Front left - spawnEdgeParticles(player, world, maxX, minY, minZ, maxX, maxY, minZ, step, dustOptions); // Front right - spawnEdgeParticles(player, world, minX, minY, maxZ, minX, maxY, maxZ, step, dustOptions); // Back left - spawnEdgeParticles(player, world, maxX, minY, maxZ, maxX, maxY, maxZ, step, dustOptions); // Back right - } - - private void spawnEdgeParticles(Player player, World world, - double x1, double y1, double z1, - double x2, double y2, double z2, - double step, Particle.DustOptions dustOptions) { - double distance = Math.sqrt( - Math.pow(x2 - x1, 2) + - Math.pow(y2 - y1, 2) + - Math.pow(z2 - z1, 2) - ); - - int count = (int) (distance / step) + 1; - - for (int i = 0; i <= count; i++) { - double t = (double) i / count; - double x = x1 + (x2 - x1) * t + 0.5; - double y = y1 + (y2 - y1) * t + 0.5; - double z = z1 + (z2 - z1) * t + 0.5; - - Location loc = new Location(world, x, y, z); - if (loc.getWorld() != null && loc.getWorld().equals(player.getWorld())) { - player.spawnParticle(Particle.DUST, loc, 1, dustOptions); - } - } - } - - private void showOutlineParticles(Player player, Location pos1, Location pos2, Color color, int density) { - World world = pos1.getWorld(); - if (world == null || !world.equals(player.getWorld())) return; - - Particle.DustOptions dustOptions = new Particle.DustOptions(color, 0.5f); - - int minX = Math.min(pos1.getBlockX(), pos2.getBlockX()); - int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX()); - int minY = Math.min(pos1.getBlockY(), pos2.getBlockY()); - int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY()); - int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ()); - int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ()); - - // Just show corners for non-active slots (lighter) - Location[] corners = { - new Location(world, minX + 0.5, minY + 0.5, minZ + 0.5), - new Location(world, maxX + 0.5, minY + 0.5, minZ + 0.5), - new Location(world, minX + 0.5, minY + 0.5, maxZ + 0.5), - new Location(world, maxX + 0.5, minY + 0.5, maxZ + 0.5), - new Location(world, minX + 0.5, maxY + 0.5, minZ + 0.5), - new Location(world, maxX + 0.5, maxY + 0.5, minZ + 0.5), - new Location(world, minX + 0.5, maxY + 0.5, maxZ + 0.5), - new Location(world, maxX + 0.5, maxY + 0.5, maxZ + 0.5) - }; - - for (Location corner : corners) { - player.spawnParticle(Particle.DUST, corner, 1, dustOptions); - } - } } diff --git a/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java b/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java index 10d3471..c5c21fc 100644 --- a/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java +++ b/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java @@ -1,7 +1,7 @@ package dev.loki.lomines.command; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.Mine; +import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.Mines; import net.kyori.adventure.text.Component; import org.bukkit.command.CommandSender;