From 9692965a3e16d524428b7eac1de357d1598cf33d Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 31 May 2026 23:34:58 +0200 Subject: [PATCH] Add particle visualization for wand selection - Add WandParticleService to show particles around selected wand areas - Show green particles at corner 1 (pos1) - Show cyan particles at corner 2 (pos2) - Show yellow/green edge particles around the selected cuboid - Show gray outline for other ready slots (lower intensity) - Particles visible only to the player holding the wand - Start particles when selection is made or wand is held - Stop particles when player switches away or disconnects --- .../java/dev/loki/lomines/LoMinesPlugin.java | 11 + .../dev/loki/lomines/gui/MineEditGui.java | 489 ++++++------------ .../listener/PlayerInteractListener.java | 34 ++ .../lomines/wand/WandParticleService.java | 228 ++++++++ 4 files changed, 431 insertions(+), 331 deletions(-) create mode 100644 src/main/java/dev/loki/lomines/wand/WandParticleService.java diff --git a/src/main/java/dev/loki/lomines/LoMinesPlugin.java b/src/main/java/dev/loki/lomines/LoMinesPlugin.java index 75dedb9..9fa705b 100644 --- a/src/main/java/dev/loki/lomines/LoMinesPlugin.java +++ b/src/main/java/dev/loki/lomines/LoMinesPlugin.java @@ -5,6 +5,7 @@ import dev.loki.lomines.core.Mines; import dev.loki.lomines.data.stats.StatsManager; import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.wand.GroupWandManager; +import dev.loki.lomines.wand.WandParticleService; import dev.lolib.commands.CommandManager; import dev.lolib.core.LoPlugin; import dev.lolib.core.dependency.DependencyManager; @@ -21,6 +22,7 @@ public final class LoMinesPlugin extends LoPlugin { private StatsManager statsManager; private CommandManager commandManager; private IntegrationManager integrationManager; + private WandParticleService wandParticleService; @Override protected void enable() { @@ -36,6 +38,7 @@ public final class LoMinesPlugin extends LoPlugin { this.statsManager = components.statsManager(); this.commandManager = components.commandManager(); this.integrationManager = components.integrationManager(); + this.wandParticleService = new WandParticleService(this); initializer.loadMines(mines); this.mineTicker = initializer.startTicker(mines); @@ -78,6 +81,10 @@ public final class LoMinesPlugin extends LoPlugin { integrationManager.shutdown(); } + if (wandParticleService != null) { + wandParticleService.stopAll(); + } + loLogger().info("LoMines has been disabled!"); } catch (Exception e) { loLogger().error("Error during plugin shutdown: " + e.getMessage()); @@ -111,4 +118,8 @@ public final class LoMinesPlugin extends LoPlugin { public IntegrationManager getIntegrationManager() { return integrationManager; } + + public WandParticleService getWandParticleService() { + return wandParticleService; + } } diff --git a/src/main/java/dev/loki/lomines/gui/MineEditGui.java b/src/main/java/dev/loki/lomines/gui/MineEditGui.java index 40b4b5e..f466978 100644 --- a/src/main/java/dev/loki/lomines/gui/MineEditGui.java +++ b/src/main/java/dev/loki/lomines/gui/MineEditGui.java @@ -3,6 +3,7 @@ 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; @@ -26,7 +27,7 @@ public final class MineEditGui { private static final int SIZE = 54; private static final String TITLE_PREFIX = "Редактор шахты: "; - // Slot constants - organized in visual groups + // Slot constants public static final int SLOT_BACK = 49; public static final int SLOT_BLOCKS = 10; public static final int SLOT_REGIONS = 12; @@ -39,44 +40,28 @@ public final class MineEditGui { public static final int SLOT_INFO = 40; // UI Constants - private static final String CLICK_EDIT = "§e▸ Нажмите для редактирования"; - private static final String CLICK_BACK = "§c▸ Нажмите чтобы закрыть"; - private static final String CLICK_SAVE = "§a▸ Нажмите для сохранения"; - private static final String CLICK_DELETE = "§c▸ Нажмите для удаления"; - private static final String SECTION_CONTENT = "§7Содержимое:"; - private static final String SECTION_SETTINGS = "§7Настройки:"; - private static final String SEPARATOR = "§8────────────────"; - private static final String COMMANDS_HEADER = "§8Команды:"; + 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() { - } + private MineEditGui() {} - /** - * Opens the mine edit GUI for the specified mine. - * - * @param plugin the plugin instance - * @param player the player to open for - * @param mineName the name of the mine to edit - */ 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); } - /** - * Refreshes the GUI contents. - */ public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv, String mineName) { Mine mine = plugin.getMines().find(mineName).orElse(null); if (mine == null) { @@ -88,21 +73,13 @@ public final class MineEditGui { private static void fill(LoMinesPlugin plugin, Mine mine, Inventory inv) { MineConfig config = mine.getConfig(); - - // Clear and fill background - for (int i = 0; i < SIZE; i++) { - inv.setItem(i, filler()); - } - - // Main editors + 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)); - - // Actions inv.setItem(SLOT_SAVE, saveItem()); inv.setItem(SLOT_DELETE, deleteItem(mine.getName())); inv.setItem(SLOT_INFO, infoItem(mine)); @@ -119,335 +96,193 @@ public final class MineEditGui { return pane; } - /** - * Formats location as short readable string. - */ - private static String formatLocation(Location loc) { - if (loc == null) return "§7не задана"; - return String.format("§7%.0f §8/§7 %.0f §8/§7 %.0f §8(§7%s§8)", - loc.getX(), loc.getY(), loc.getZ(), loc.getWorld().getName()); + 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()); } - /** - * Truncates string for preview display. - */ - private static String truncate(String str, int maxLen) { - if (str == null || str.isEmpty()) return "§7не задан"; - if (str.length() <= maxLen) return str; - return str.substring(0, maxLen - 3) + "..."; + 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) { - ItemStack stack = new ItemStack(Material.DIAMOND_PICKAXE); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("§a§lБлоки", NamedTextColor.GREEN)); - - int blockCount = config.blocks().blockCount(); - String fillMode = config.blocks().fillMode().name(); - boolean hasCustom = config.blocks().hasCustomBlocks(); - - List lore = new ArrayList<>(); - lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); - lore.add(Component.text(SECTION_SETTINGS, NamedTextColor.GRAY)); - lore.add(Component.text(" §fТипов: §7" + blockCount, NamedTextColor.WHITE)); - lore.add(Component.text(" §fРежим: §7" + fillMode, NamedTextColor.WHITE)); - lore.add(Component.text(" §fКастомные: " + (hasCustom ? "§aДа" : "§7Нет"), NamedTextColor.WHITE)); - - // Show first few blocks as preview - var weights = config.blocks().weights(); - if (!weights.isEmpty()) { - lore.add(Component.empty()); - lore.add(Component.text("§7Топ блоков:", NamedTextColor.GRAY)); - weights.entrySet().stream() - .sorted((a, b) -> Double.compare(b.getValue(), a.getValue())) - .limit(3) - .forEach(e -> lore.add(Component.text( - String.format(" §8• §7%s §8(§7%.0f%%§8)", - e.getKey().serialize(), e.getValue() * 100), - NamedTextColor.GRAY))); - } - - lore.add(Component.empty()); - lore.add(Component.text(CLICK_EDIT, NamedTextColor.YELLOW)); - - meta.lore(lore); - stack.setItemMeta(meta); + 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))); } - return stack; + lore.add(""); + lore.add(EDIT); + return create(Material.DIAMOND_PICKAXE, "§a§lБлоки", lore.toArray(new String[0])); } private static ItemStack regionsItem(MineConfig config) { - ItemStack stack = new ItemStack(Material.COMPASS); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("§a§lРегионы", NamedTextColor.GREEN)); - - int regionCount = config.region().regionCount(); - int volume = config.region().totalVolume(); - - List lore = new ArrayList<>(); - lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); - lore.add(Component.text(SECTION_SETTINGS, NamedTextColor.GRAY)); - lore.add(Component.text(" §fКоличество: §7" + regionCount, NamedTextColor.WHITE)); - lore.add(Component.text(" §fОбщий объём: §7" + volume + " §8блоков", NamedTextColor.WHITE)); - - // Show world info - lore.add(Component.empty()); - lore.add(Component.text(" §fМир: §7" + config.worldName(), NamedTextColor.WHITE)); - - lore.add(Component.empty()); - lore.add(Component.text("§8Изменяется через палочку", NamedTextColor.DARK_GRAY)); - - meta.lore(lore); - stack.setItemMeta(meta); - } - return stack; + 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) { - ItemStack stack = new ItemStack(Material.CLOCK); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("§a§lСброс", NamedTextColor.GREEN)); - - String interval = config.reset().intervalDisplay(); - double percent = config.reset().percentTrigger(); - boolean percentEnabled = config.reset().isPercentTriggerEnabled(); - int cmdCount = config.reset().commands().size(); - String broadcast = config.reset().broadcastMessage(); - - List lore = new ArrayList<>(); - lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); - lore.add(Component.text(SECTION_SETTINGS, NamedTextColor.GRAY)); - lore.add(Component.text(" §fИнтервал: §7" + interval, NamedTextColor.WHITE)); - lore.add(Component.text(" §fТриггер по %: " + (percentEnabled ? "§a" + percent + "%" : "§7выключен"), NamedTextColor.WHITE)); - lore.add(Component.text(" §fКоманд: §7" + cmdCount, NamedTextColor.WHITE)); - - // Show broadcast status - lore.add(Component.empty()); - lore.add(Component.text(" §fСообщение: " + (broadcast.isEmpty() ? "§7нет" : "§aесть"), NamedTextColor.WHITE)); - if (!broadcast.isEmpty()) { - lore.add(Component.text(" §8" + truncate(broadcast, 25), NamedTextColor.DARK_GRAY)); - } - - lore.add(Component.empty()); - lore.add(Component.text(CLICK_EDIT, NamedTextColor.YELLOW)); - - meta.lore(lore); - stack.setItemMeta(meta); + 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)); } - return stack; + lore.add(""); + lore.add(EDIT); + return create(Material.CLOCK, "§a§lСброс", lore.toArray(new String[0])); } private static ItemStack rewardsItem(MineConfig config) { - ItemStack stack = new ItemStack(Material.GOLD_INGOT); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("§a§lНаграды", NamedTextColor.GREEN)); - - var entries = config.rewards().entries(); - int rewardCount = entries.size(); - - // Count reward types - long itemRewards = entries.stream() - .flatMap(e -> e.items().stream()).count(); - long cmdRewards = entries.stream() - .filter(e -> !e.commands().isEmpty()).count(); - long preventDrops = entries.stream() - .filter(RewardConfig.RewardEntry::preventVanillaDrops).count(); - - List lore = new ArrayList<>(); - lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); - lore.add(Component.text(SECTION_SETTINGS, NamedTextColor.GRAY)); - lore.add(Component.text(" §fЗаписей: §7" + rewardCount, NamedTextColor.WHITE)); - - if (rewardCount > 0) { - lore.add(Component.empty()); - lore.add(Component.text("§7Типы наград:", NamedTextColor.GRAY)); - lore.add(Component.text(" §8• §fПредметов: §7" + itemRewards, NamedTextColor.WHITE)); - lore.add(Component.text(" §8• §fКоманд: §7" + cmdRewards, NamedTextColor.WHITE)); - if (preventDrops > 0) { - lore.add(Component.text(" §8• §cБлокируют дроп: §7" + preventDrops, NamedTextColor.WHITE)); - } - } - - lore.add(Component.empty()); - lore.add(Component.text(CLICK_EDIT, NamedTextColor.YELLOW)); - - meta.lore(lore); - stack.setItemMeta(meta); + 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); } - return stack; + lore.add(""); + lore.add(EDIT); + return create(Material.GOLD_INGOT, "§a§lНаграды", lore.toArray(new String[0])); } private static ItemStack teleportItem(MineConfig config) { - ItemStack stack = new ItemStack(Material.ENDER_PEARL); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("§a§lТелепорт", NamedTextColor.GREEN)); - - boolean tpEnabled = config.teleport().enabled(); - boolean spawnEnabled = config.playerSpawn().enabled(); - - List lore = new ArrayList<>(); - lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); - lore.add(Component.text(SECTION_SETTINGS, NamedTextColor.GRAY)); - - // Teleport location - lore.add(Component.text(" §fТелепорт: " + (tpEnabled ? "§aвкл" : "§7выкл"), NamedTextColor.WHITE)); - if (tpEnabled) { - config.teleport().getLocation().ifPresent(loc -> - lore.add(Component.text(" " + formatLocation(loc), NamedTextColor.WHITE))); - } - - // Spawn location (for stuck players) - lore.add(Component.empty()); - lore.add(Component.text(" §fСпавн (застрявших): " + (spawnEnabled ? "§aвкл" : "§7выкл"), NamedTextColor.WHITE)); - if (spawnEnabled) { - config.playerSpawn().getLocation().ifPresent(loc -> - lore.add(Component.text(" " + formatLocation(loc), NamedTextColor.WHITE))); - } - - // Command hints - lore.add(Component.empty()); - lore.add(Component.text(COMMANDS_HEADER, NamedTextColor.DARK_GRAY)); - lore.add(Component.text(" §8• §7/lm setteleport §f", NamedTextColor.GRAY)); - lore.add(Component.text(" §8• §7/lm setspawn §f", NamedTextColor.GRAY)); - lore.add(Component.text(" §8• §7/lm clearspawn §f", NamedTextColor.GRAY)); - - lore.add(Component.empty()); - lore.add(Component.text(CLICK_EDIT, NamedTextColor.YELLOW)); - - meta.lore(lore); - stack.setItemMeta(meta); + 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))); } - return stack; + 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) { - ItemStack stack = new ItemStack(Material.PAINTING); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("§a§lИнтерфейс", NamedTextColor.GREEN)); - - boolean actionBarEnabled = config.ui().actionBarEnabled(); - double range = config.ui().actionBarRange(); - String format = config.ui().actionBarFormat(); - String timerFmt = config.ui().timerFormat(); - - List lore = new ArrayList<>(); - lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); - lore.add(Component.text(SECTION_SETTINGS, NamedTextColor.GRAY)); - lore.add(Component.text(" §fAction Bar: " + (actionBarEnabled ? "§aвкл" : "§7выкл"), NamedTextColor.WHITE)); - lore.add(Component.text(" §fРадиус: §7" + (int) range + " §8блоков", NamedTextColor.WHITE)); - - // Show format previews - lore.add(Component.empty()); - lore.add(Component.text("§7Формат ActionBar:", NamedTextColor.GRAY)); - lore.add(Component.text(" §8" + truncate(format, 28), NamedTextColor.DARK_GRAY)); - lore.add(Component.empty()); - lore.add(Component.text("§7Формат таймера: §8" + timerFmt, NamedTextColor.GRAY)); - - lore.add(Component.empty()); - lore.add(Component.text(CLICK_EDIT, NamedTextColor.YELLOW)); - - meta.lore(lore); - stack.setItemMeta(meta); - } - return stack; + 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() { - ItemStack stack = new ItemStack(Material.LIME_DYE); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("§a§lСохранить изменения", NamedTextColor.GREEN)); - List lore = new ArrayList<>(); - lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); - lore.add(Component.text("§7Сохраняет конфигурацию шахты", NamedTextColor.GRAY)); - lore.add(Component.text("§7в файл на диске", NamedTextColor.GRAY)); - lore.add(Component.empty()); - lore.add(Component.text(CLICK_SAVE, NamedTextColor.GREEN)); - meta.lore(lore); - stack.setItemMeta(meta); - } - return stack; + return create(Material.LIME_DYE, "§a§lСохранить изменения", + SEP, + "§7Сохраняет конфигурацию шахты", + "§7в файл на диске", + "", + SAVE + ); } private static ItemStack deleteItem(String mineName) { - ItemStack stack = new ItemStack(Material.TNT); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("§c§lУдалить шахту", NamedTextColor.RED)); - List lore = new ArrayList<>(); - lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); - lore.add(Component.text("§c§lВнимание!", NamedTextColor.RED)); - lore.add(Component.text("§cЭто действие нельзя отменить!", NamedTextColor.RED)); - lore.add(Component.empty()); - lore.add(Component.text("§7Шахта: §f" + mineName, NamedTextColor.GRAY)); - lore.add(Component.empty()); - lore.add(Component.text(CLICK_DELETE, NamedTextColor.RED)); - meta.lore(lore); - stack.setItemMeta(meta); - } - return stack; + return create(Material.TNT, "§c§lУдалить шахту", + SEP, + "§c§lВнимание!", + "§cЭто действие нельзя отменить!", + "", + "§7Шахта: §f" + mineName, + "", + DEL + ); } private static ItemStack infoItem(Mine mine) { - ItemStack stack = new ItemStack(Material.BOOK); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("§b§lИнформация", NamedTextColor.AQUA)); - - int blocks = mine.getBlocks(); - int total = mine.getTotalVolume(); - double percent = mine.getPercentFilled(); - String statusColor = percent > 50 ? "§a" : percent > 25 ? "§e" : "§c"; - - List lore = new ArrayList<>(); - lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); - lore.add(Component.text("§7Базовые данные:", NamedTextColor.GRAY)); - lore.add(Component.text(" §fНазвание: §7" + mine.getName(), NamedTextColor.WHITE)); - lore.add(Component.text(" §fМир: §7" + mine.getConfig().worldName(), NamedTextColor.WHITE)); - - lore.add(Component.empty()); - lore.add(Component.text("§7Состояние заполнения:", NamedTextColor.GRAY)); - lore.add(Component.text(" §fБлоков: " + statusColor + blocks + "§8/§7" + total, NamedTextColor.WHITE)); - lore.add(Component.text(" §fПроцент: " + statusColor + String.format("%.1f%%", percent), NamedTextColor.WHITE)); - - // Visual percentage bar - int barLen = 10; - int filled = (int) Math.round(percent / 100.0 * barLen); - String bar = "§a" + "█".repeat(filled) + "§8" + "░".repeat(barLen - filled); - lore.add(Component.text(" " + bar, NamedTextColor.WHITE)); - - meta.lore(lore); - stack.setItemMeta(meta); - } - return stack; + 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() { - ItemStack stack = new ItemStack(Material.BARRIER); - ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("§cЗакрыть", NamedTextColor.RED)); - stack.setItemMeta(meta); - } - return stack; + return create(Material.ARROW, "§c§lЗакрыть", + SEP, + "§7Закрыть редактор", + "", + CLOSE + ); } - /** - * Handles clicks in the GUI. - * - * @return true if the event should be cancelled - */ public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, String mineName) { - if (rawSlot < 0 || rawSlot >= SIZE) { - return false; - } - + if (rawSlot < 0 || rawSlot >= SIZE) return false; switch (rawSlot) { case SLOT_BACK -> { player.closeInventory(); @@ -455,27 +290,22 @@ public final class MineEditGui { } case SLOT_BLOCKS -> { player.sendMessage(Component.text("Открытие редактора блоков... (в разработке)", NamedTextColor.YELLOW)); - // TODO: Open BlockEditGui return true; } case SLOT_RESET -> { player.sendMessage(Component.text("Открытие редактора сброса... (в разработке)", NamedTextColor.YELLOW)); - // TODO: Open ResetEditGui return true; } case SLOT_REWARDS -> { player.sendMessage(Component.text("Открытие редактора наград... (в разработке)", NamedTextColor.YELLOW)); - // TODO: Open RewardEditGui return true; } case SLOT_TELEPORT -> { player.sendMessage(Component.text("Открытие редактора телепорта... (в разработке)", NamedTextColor.YELLOW)); - // TODO: Open TeleportEditGui return true; } case SLOT_UI -> { player.sendMessage(Component.text("Открытие редактора интерфейса... (в разработке)", NamedTextColor.YELLOW)); - // TODO: Open UIEditGui return true; } case SLOT_SAVE -> { @@ -484,16 +314,13 @@ public final class MineEditGui { return true; } case SLOT_DELETE -> { - // Open confirmation GUI ConfirmDeleteGui.open(plugin, player, mineName); return true; } case SLOT_INFO, SLOT_REGIONS -> { - // Info only, no action return true; } } - - return true; // Cancel all other clicks + return true; } } diff --git a/src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java b/src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java index caaa397..a785375 100644 --- a/src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java +++ b/src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java @@ -13,10 +13,13 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerItemHeldEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.EquipmentSlot; /** * Group wand: ЛКМ / ПКМ по блоку — углы; Shift+ПКМ — GUI группы. + * Also starts/stops particle visualization when holding the wand. */ public final class PlayerInteractListener implements Listener { @@ -54,6 +57,9 @@ public final class PlayerInteractListener implements Listener { session.setPos1(slot, block.getLocation()); player.sendMessage(Component.text( "Угол 1 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN)); + + // Start showing particles when selection is made + plugin.getWandParticleService().startShowingParticles(player); return; } @@ -68,6 +74,34 @@ public final class PlayerInteractListener implements Listener { session.setPos2(slot, block.getLocation()); player.sendMessage(Component.text( "Угол 2 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN)); + + // Start showing particles when selection is made + plugin.getWandParticleService().startShowingParticles(player); } } + + /** + * Starts/stops particle visualization when player switches items. + */ + @EventHandler + public void onItemHeld(PlayerItemHeldEvent event) { + Player player = event.getPlayer(); + + // Check if player switched to wand + if (GroupWandItem.isWand(plugin, player.getInventory().getItem(event.getNewSlot()))) { + plugin.getWandParticleService().startShowingParticles(player); + } + // Check if player switched away from wand + else if (GroupWandItem.isWand(plugin, player.getInventory().getItem(event.getPreviousSlot()))) { + plugin.getWandParticleService().stopShowingParticles(player.getUniqueId()); + } + } + + /** + * Stops particles when player disconnects. + */ + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + plugin.getWandParticleService().stopShowingParticles(event.getPlayer().getUniqueId()); + } } diff --git a/src/main/java/dev/loki/lomines/wand/WandParticleService.java b/src/main/java/dev/loki/lomines/wand/WandParticleService.java new file mode 100644 index 0000000..902b582 --- /dev/null +++ b/src/main/java/dev/loki/lomines/wand/WandParticleService.java @@ -0,0 +1,228 @@ +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; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Shows particles around wand selection areas to visualize the selected region. + * Particles are shown only to the player holding the wand. + */ +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 + + 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() { + @Override + public void run() { + if (!player.isOnline() || !isHoldingWand(player)) { + cancel(); + activeTasks.remove(playerId); + return; + } + + showParticles(player); + } + }; + + task.runTaskTimer(plugin, 0L, 10L); // Every 10 ticks (0.5 seconds) + 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(); + } + } + + /** + * Stops all particle tasks. + */ + public void stopAll() { + for (BukkitRunnable task : activeTasks.values()) { + task.cancel(); + } + activeTasks.clear(); + } + + private boolean isHoldingWand(Player player) { + return GroupWandItem.isWand(plugin, player.getInventory().getItemInMainHand()); + } + + private void showParticles(Player player) { + GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId()); + int activeSlot = session.getActiveSlot(); + + 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); + } + + // Show particles for corner 2 if set + if (pos2 != null && pos2.getWorld() != null) { + 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); + } + } + + // Also show other slots that are ready (with lower intensity) + 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); + } + } + } + } + + 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); + } + } +}