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
This commit is contained in:
loki5512344 2026-05-31 23:34:58 +02:00
parent 7d0f6a283d
commit 9692965a3e
4 changed files with 431 additions and 331 deletions

View file

@ -5,6 +5,7 @@ import dev.loki.lomines.core.Mines;
import dev.loki.lomines.data.stats.StatsManager; import dev.loki.lomines.data.stats.StatsManager;
import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.integration.IntegrationManager;
import dev.loki.lomines.wand.GroupWandManager; import dev.loki.lomines.wand.GroupWandManager;
import dev.loki.lomines.wand.WandParticleService;
import dev.lolib.commands.CommandManager; import dev.lolib.commands.CommandManager;
import dev.lolib.core.LoPlugin; import dev.lolib.core.LoPlugin;
import dev.lolib.core.dependency.DependencyManager; import dev.lolib.core.dependency.DependencyManager;
@ -21,6 +22,7 @@ public final class LoMinesPlugin extends LoPlugin {
private StatsManager statsManager; private StatsManager statsManager;
private CommandManager commandManager; private CommandManager commandManager;
private IntegrationManager integrationManager; private IntegrationManager integrationManager;
private WandParticleService wandParticleService;
@Override @Override
protected void enable() { protected void enable() {
@ -36,6 +38,7 @@ public final class LoMinesPlugin extends LoPlugin {
this.statsManager = components.statsManager(); this.statsManager = components.statsManager();
this.commandManager = components.commandManager(); this.commandManager = components.commandManager();
this.integrationManager = components.integrationManager(); this.integrationManager = components.integrationManager();
this.wandParticleService = new WandParticleService(this);
initializer.loadMines(mines); initializer.loadMines(mines);
this.mineTicker = initializer.startTicker(mines); this.mineTicker = initializer.startTicker(mines);
@ -78,6 +81,10 @@ public final class LoMinesPlugin extends LoPlugin {
integrationManager.shutdown(); integrationManager.shutdown();
} }
if (wandParticleService != null) {
wandParticleService.stopAll();
}
loLogger().info("LoMines has been disabled!"); loLogger().info("LoMines has been disabled!");
} catch (Exception e) { } catch (Exception e) {
loLogger().error("Error during plugin shutdown: " + e.getMessage()); loLogger().error("Error during plugin shutdown: " + e.getMessage());
@ -111,4 +118,8 @@ public final class LoMinesPlugin extends LoPlugin {
public IntegrationManager getIntegrationManager() { public IntegrationManager getIntegrationManager() {
return integrationManager; return integrationManager;
} }
public WandParticleService getWandParticleService() {
return wandParticleService;
}
} }

View file

@ -3,6 +3,7 @@ package dev.loki.lomines.gui;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine; import dev.loki.lomines.core.Mine;
import dev.loki.lomines.data.config.MineConfig; 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.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@ -26,7 +27,7 @@ public final class MineEditGui {
private static final int SIZE = 54; private static final int SIZE = 54;
private static final String TITLE_PREFIX = "Редактор шахты: "; 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_BACK = 49;
public static final int SLOT_BLOCKS = 10; public static final int SLOT_BLOCKS = 10;
public static final int SLOT_REGIONS = 12; public static final int SLOT_REGIONS = 12;
@ -39,44 +40,28 @@ public final class MineEditGui {
public static final int SLOT_INFO = 40; public static final int SLOT_INFO = 40;
// UI Constants // UI Constants
private static final String CLICK_EDIT = "§e▸ Нажмите для редактирования"; private static final String SEP = "§8───────────────";
private static final String CLICK_BACK = "§c▸ Нажмите чтобы закрыть"; private static final String EDIT = "§e▸ Нажмите для редактирования";
private static final String CLICK_SAVE = "§a▸ Нажмите для сохранения"; private static final String CLOSE = "§c▸ Нажмите чтобы закрыть";
private static final String CLICK_DELETE = "§c▸ Нажмите для удаления"; private static final String SAVE = "§a▸ Нажмите для сохранения";
private static final String SECTION_CONTENT = "§7Содержимое:"; private static final String DEL = "§c▸ Нажмите для удаления";
private static final String SECTION_SETTINGS = "§7Настройки:";
private static final String SEPARATOR = "§8────────────────";
private static final String COMMANDS_HEADER = "§8Команды:";
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) { public static void open(LoMinesPlugin plugin, Player player, String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null); Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) { if (mine == null) {
player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
return; return;
} }
MineEditGuiHolder holder = new MineEditGuiHolder(player.getUniqueId(), mineName); MineEditGuiHolder holder = new MineEditGuiHolder(player.getUniqueId(), mineName);
Inventory inv = Bukkit.createInventory(holder, SIZE, Inventory inv = Bukkit.createInventory(holder, SIZE,
Component.text(TITLE_PREFIX + mineName, NamedTextColor.DARK_GREEN)); Component.text(TITLE_PREFIX + mineName, NamedTextColor.DARK_GREEN));
holder.setInventory(inv); holder.setInventory(inv);
fill(plugin, mine, inv); fill(plugin, mine, inv);
player.openInventory(inv); player.openInventory(inv);
} }
/**
* Refreshes the GUI contents.
*/
public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv, String mineName) { public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv, String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null); Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) { if (mine == null) {
@ -88,21 +73,13 @@ public final class MineEditGui {
private static void fill(LoMinesPlugin plugin, Mine mine, Inventory inv) { private static void fill(LoMinesPlugin plugin, Mine mine, Inventory inv) {
MineConfig config = mine.getConfig(); MineConfig config = mine.getConfig();
for (int i = 0; i < SIZE; i++) inv.setItem(i, filler());
// Clear and fill background
for (int i = 0; i < SIZE; i++) {
inv.setItem(i, filler());
}
// Main editors
inv.setItem(SLOT_BLOCKS, blocksItem(config)); inv.setItem(SLOT_BLOCKS, blocksItem(config));
inv.setItem(SLOT_REGIONS, regionsItem(config)); inv.setItem(SLOT_REGIONS, regionsItem(config));
inv.setItem(SLOT_RESET, resetItem(config)); inv.setItem(SLOT_RESET, resetItem(config));
inv.setItem(SLOT_REWARDS, rewardsItem(config)); inv.setItem(SLOT_REWARDS, rewardsItem(config));
inv.setItem(SLOT_TELEPORT, teleportItem(config)); inv.setItem(SLOT_TELEPORT, teleportItem(config));
inv.setItem(SLOT_UI, uiItem(config)); inv.setItem(SLOT_UI, uiItem(config));
// Actions
inv.setItem(SLOT_SAVE, saveItem()); inv.setItem(SLOT_SAVE, saveItem());
inv.setItem(SLOT_DELETE, deleteItem(mine.getName())); inv.setItem(SLOT_DELETE, deleteItem(mine.getName()));
inv.setItem(SLOT_INFO, infoItem(mine)); inv.setItem(SLOT_INFO, infoItem(mine));
@ -119,335 +96,193 @@ public final class MineEditGui {
return pane; return pane;
} }
/** private static String loc(Location l) {
* Formats location as short readable string. 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 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 trunc(String s, int max) {
* Truncates string for preview display. if (s == null || s.isEmpty()) return "§7нет";
*/ return s.length() > max ? s.substring(0, max - 3) + "..." : s;
private static String truncate(String str, int maxLen) { }
if (str == null || str.isEmpty()) return "§7не задан";
if (str.length() <= maxLen) return str; private static ItemStack create(Material mat, String name, String... loreLines) {
return str.substring(0, maxLen - 3) + "..."; ItemStack stack = new ItemStack(mat);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
meta.displayName(Component.text(name, NamedTextColor.WHITE));
List<Component> 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) { private static ItemStack blocksItem(MineConfig config) {
ItemStack stack = new ItemStack(Material.DIAMOND_PICKAXE); var w = config.blocks().weights();
ItemMeta meta = stack.getItemMeta(); List<String> lore = new ArrayList<>();
if (meta != null) { lore.add(SEP);
meta.displayName(Component.text("§a§lБлоки", NamedTextColor.GREEN)); lore.add("§7Настройки:");
lore.add(" §fТипов: §7" + config.blocks().blockCount());
int blockCount = config.blocks().blockCount(); lore.add(" §fРежим: §7" + config.blocks().fillMode().name());
String fillMode = config.blocks().fillMode().name(); lore.add(" §fКастомные: " + (config.blocks().hasCustomBlocks() ? "§aДа" : "§7Нет"));
boolean hasCustom = config.blocks().hasCustomBlocks(); if (!w.isEmpty()) {
lore.add("");
List<Component> lore = new ArrayList<>(); lore.add("§7Топ блоков:");
lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); w.entrySet().stream().sorted((a, b) -> Double.compare(b.getValue(), a.getValue())).limit(3)
lore.add(Component.text(SECTION_SETTINGS, NamedTextColor.GRAY)); .forEach(e -> lore.add(String.format(" §8• §7%s §8(§7%.0f%%§8)", e.getKey().serialize(), e.getValue() * 100)));
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("");
lore.add(Component.empty()); lore.add(EDIT);
lore.add(Component.text(CLICK_EDIT, NamedTextColor.YELLOW)); return create(Material.DIAMOND_PICKAXE, "§a§lБлоки", lore.toArray(new String[0]));
meta.lore(lore);
stack.setItemMeta(meta);
}
return stack;
} }
private static ItemStack regionsItem(MineConfig config) { private static ItemStack regionsItem(MineConfig config) {
ItemStack stack = new ItemStack(Material.COMPASS); return create(Material.COMPASS, "§a§lРегионы",
ItemMeta meta = stack.getItemMeta(); SEP,
if (meta != null) { "§7Настройки:",
meta.displayName(Component.text("§a§lРегионы", NamedTextColor.GREEN)); " §fКоличество: §7" + config.region().regionCount(),
" §fОбщий объём: §7" + config.region().totalVolume() + " §8блоков",
int regionCount = config.region().regionCount(); "",
int volume = config.region().totalVolume(); " §fМир: §7" + config.worldName(),
"",
List<Component> lore = new ArrayList<>(); "§8Изменяется через палочку"
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;
} }
private static ItemStack resetItem(MineConfig config) { private static ItemStack resetItem(MineConfig config) {
ItemStack stack = new ItemStack(Material.CLOCK); List<String> lore = new ArrayList<>();
ItemMeta meta = stack.getItemMeta(); lore.add(SEP);
if (meta != null) { lore.add("§7Настройки:");
meta.displayName(Component.text("§a§lСброс", NamedTextColor.GREEN)); lore.add(" §fИнтервал: §7" + config.reset().intervalDisplay());
lore.add(" §fТриггер по %: " + (config.reset().isPercentTriggerEnabled() ? "§a" + config.reset().percentTrigger() + "%" : "§7выкл"));
String interval = config.reset().intervalDisplay(); lore.add(" §fКоманд: §7" + config.reset().commands().size());
double percent = config.reset().percentTrigger(); lore.add("");
boolean percentEnabled = config.reset().isPercentTriggerEnabled(); lore.add(" §fСообщение: " + (config.reset().broadcastMessage().isEmpty() ? "§7нет" : "§aесть"));
int cmdCount = config.reset().commands().size(); if (!config.reset().broadcastMessage().isEmpty()) {
String broadcast = config.reset().broadcastMessage(); lore.add(" §8" + trunc(config.reset().broadcastMessage(), 25));
List<Component> 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("");
lore.add(Component.empty()); lore.add(EDIT);
lore.add(Component.text(CLICK_EDIT, NamedTextColor.YELLOW)); return create(Material.CLOCK, "§a§lСброс", lore.toArray(new String[0]));
meta.lore(lore);
stack.setItemMeta(meta);
}
return stack;
} }
private static ItemStack rewardsItem(MineConfig config) { 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(); var entries = config.rewards().entries();
int rewardCount = entries.size(); long items = entries.stream().flatMap(e -> e.items().stream()).count();
long cmds = entries.stream().filter(e -> !e.commands().isEmpty()).count();
// Count reward types long blockDrop = entries.stream().filter(RewardConfig.RewardEntry::preventVanillaDrops).count();
long itemRewards = entries.stream() List<String> lore = new ArrayList<>();
.flatMap(e -> e.items().stream()).count(); lore.add(SEP);
long cmdRewards = entries.stream() lore.add("§7Настройки:");
.filter(e -> !e.commands().isEmpty()).count(); lore.add(" §fЗаписей: §7" + entries.size());
long preventDrops = entries.stream() if (!entries.isEmpty()) {
.filter(RewardConfig.RewardEntry::preventVanillaDrops).count(); lore.add("");
lore.add("§7Типы наград:");
List<Component> lore = new ArrayList<>(); lore.add(" §8• §fПредметов: §7" + items);
lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); lore.add(" §8• §fКоманд: §7" + cmds);
lore.add(Component.text(SECTION_SETTINGS, NamedTextColor.GRAY)); if (blockDrop > 0) lore.add(" §8• §cБлокируют дроп: §7" + blockDrop);
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("");
lore.add(EDIT);
lore.add(Component.empty()); return create(Material.GOLD_INGOT, "§a§lНаграды", lore.toArray(new String[0]));
lore.add(Component.text(CLICK_EDIT, NamedTextColor.YELLOW));
meta.lore(lore);
stack.setItemMeta(meta);
}
return stack;
} }
private static ItemStack teleportItem(MineConfig config) { private static ItemStack teleportItem(MineConfig config) {
ItemStack stack = new ItemStack(Material.ENDER_PEARL); List<String> lore = new ArrayList<>();
ItemMeta meta = stack.getItemMeta(); lore.add(SEP);
if (meta != null) { lore.add("§7Настройки:");
meta.displayName(Component.text("§a§lТелепорт", NamedTextColor.GREEN)); lore.add(" §fТелепорт: " + (config.teleport().enabled() ? "§aвкл" : "§7выкл"));
if (config.teleport().enabled()) {
boolean tpEnabled = config.teleport().enabled(); config.teleport().getLocation().ifPresent(loc -> lore.add(" " + loc(loc)));
boolean spawnEnabled = config.playerSpawn().enabled();
List<Component> 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)));
} }
lore.add("");
// Spawn location (for stuck players) lore.add(" §fСпавн (застрявших): " + (config.playerSpawn().enabled() ? "§aвкл" : "§7выкл"));
lore.add(Component.empty()); if (config.playerSpawn().enabled()) {
lore.add(Component.text(" §fСпавн (застрявших): " + (spawnEnabled ? "§aвкл" : "§7выкл"), NamedTextColor.WHITE)); config.playerSpawn().getLocation().ifPresent(loc -> lore.add(" " + loc(loc)));
if (spawnEnabled) {
config.playerSpawn().getLocation().ifPresent(loc ->
lore.add(Component.text(" " + formatLocation(loc), NamedTextColor.WHITE)));
} }
lore.add("");
// Command hints lore.add("§8Команды:");
lore.add(Component.empty()); lore.add(" §8• §7/lm setteleport §f<mine>");
lore.add(Component.text(COMMANDS_HEADER, NamedTextColor.DARK_GRAY)); lore.add(" §8• §7/lm setspawn §f<mine>");
lore.add(Component.text(" §8• §7/lm setteleport §f<mine>", NamedTextColor.GRAY)); lore.add(" §8• §7/lm clearspawn §f<mine>");
lore.add(Component.text(" §8• §7/lm setspawn §f<mine>", NamedTextColor.GRAY)); lore.add("");
lore.add(Component.text(" §8• §7/lm clearspawn §f<mine>", NamedTextColor.GRAY)); lore.add(EDIT);
return create(Material.ENDER_PEARL, "§a§lТелепорт", lore.toArray(new String[0]));
lore.add(Component.empty());
lore.add(Component.text(CLICK_EDIT, NamedTextColor.YELLOW));
meta.lore(lore);
stack.setItemMeta(meta);
}
return stack;
} }
private static ItemStack uiItem(MineConfig config) { private static ItemStack uiItem(MineConfig config) {
ItemStack stack = new ItemStack(Material.PAINTING); return create(Material.PAINTING, "§a§lИнтерфейс",
ItemMeta meta = stack.getItemMeta(); SEP,
if (meta != null) { "§7Настройки:",
meta.displayName(Component.text("§a§lИнтерфейс", NamedTextColor.GREEN)); " §fAction Bar: " + (config.ui().actionBarEnabled() ? "§aвкл" : "§7выкл"),
" §fРадиус: §7" + (int) config.ui().actionBarRange() + " §8блоков",
boolean actionBarEnabled = config.ui().actionBarEnabled(); "",
double range = config.ui().actionBarRange(); "§7Формат ActionBar:",
String format = config.ui().actionBarFormat(); " §8" + trunc(config.ui().actionBarFormat(), 28),
String timerFmt = config.ui().timerFormat(); "",
"§7Формат таймера: §8" + config.ui().timerFormat(),
List<Component> lore = new ArrayList<>(); "",
lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); EDIT
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;
} }
private static ItemStack saveItem() { private static ItemStack saveItem() {
ItemStack stack = new ItemStack(Material.LIME_DYE); return create(Material.LIME_DYE, "§a§lСохранить изменения",
ItemMeta meta = stack.getItemMeta(); SEP,
if (meta != null) { "§7Сохраняет конфигурацию шахты",
meta.displayName(Component.text("§a§lСохранить изменения", NamedTextColor.GREEN)); "§7в файл на диске",
List<Component> lore = new ArrayList<>(); "",
lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); SAVE
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;
} }
private static ItemStack deleteItem(String mineName) { private static ItemStack deleteItem(String mineName) {
ItemStack stack = new ItemStack(Material.TNT); return create(Material.TNT, "§c§lУдалить шахту",
ItemMeta meta = stack.getItemMeta(); SEP,
if (meta != null) { "§c§lВнимание!",
meta.displayName(Component.text("§c§lУдалить шахту", NamedTextColor.RED)); "§cЭто действие нельзя отменить!",
List<Component> lore = new ArrayList<>(); "",
lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); "§7Шахта: §f" + mineName,
lore.add(Component.text("§c§lВнимание!", NamedTextColor.RED)); "",
lore.add(Component.text("§cЭто действие нельзя отменить!", NamedTextColor.RED)); DEL
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;
} }
private static ItemStack infoItem(Mine mine) { 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 blocks = mine.getBlocks();
int total = mine.getTotalVolume(); int total = mine.getTotalVolume();
double percent = mine.getPercentFilled(); double percent = mine.getPercentFilled();
String statusColor = percent > 50 ? "§a" : percent > 25 ? "§e" : "§c"; String c = percent > 50 ? "§a" : percent > 25 ? "§e" : "§c";
int filled = (int) Math.round(percent / 100.0 * 10);
List<Component> lore = new ArrayList<>(); String bar = "§a" + "█".repeat(filled) + "§8" + "░".repeat(10 - filled);
lore.add(Component.text(SEPARATOR, NamedTextColor.DARK_GRAY)); return create(Material.BOOK, "§b§lИнформация",
lore.add(Component.text("§7Базовые данные:", NamedTextColor.GRAY)); SEP,
lore.add(Component.text(" §fНазвание: §7" + mine.getName(), NamedTextColor.WHITE)); "§7Базовые данные:",
lore.add(Component.text(" §fМир: §7" + mine.getConfig().worldName(), NamedTextColor.WHITE)); " §fНазвание: §7" + mine.getName(),
" §fМир: §7" + mine.getConfig().worldName(),
lore.add(Component.empty()); "",
lore.add(Component.text("§7Состояние заполнения:", NamedTextColor.GRAY)); "§7Состояние заполнения:",
lore.add(Component.text(" §fБлоков: " + statusColor + blocks + "§8/§7" + total, NamedTextColor.WHITE)); " §fБлоков: " + c + blocks + "§8/§7" + total,
lore.add(Component.text(" §fПроцент: " + statusColor + String.format("%.1f%%", percent), NamedTextColor.WHITE)); " §fПроцент: " + c + String.format("%.1f%%", percent),
" " + bar
// 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;
} }
private static ItemStack backItem() { private static ItemStack backItem() {
ItemStack stack = new ItemStack(Material.BARRIER); return create(Material.ARROW, "§c§lЗакрыть",
ItemMeta meta = stack.getItemMeta(); SEP,
if (meta != null) { "§7Закрыть редактор",
meta.displayName(Component.text("§cЗакрыть", NamedTextColor.RED)); "",
stack.setItemMeta(meta); CLOSE
} );
return stack;
} }
/**
* 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) { public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, String mineName) {
if (rawSlot < 0 || rawSlot >= SIZE) { if (rawSlot < 0 || rawSlot >= SIZE) return false;
return false;
}
switch (rawSlot) { switch (rawSlot) {
case SLOT_BACK -> { case SLOT_BACK -> {
player.closeInventory(); player.closeInventory();
@ -455,27 +290,22 @@ public final class MineEditGui {
} }
case SLOT_BLOCKS -> { case SLOT_BLOCKS -> {
player.sendMessage(Component.text("Открытие редактора блоков... (в разработке)", NamedTextColor.YELLOW)); player.sendMessage(Component.text("Открытие редактора блоков... (в разработке)", NamedTextColor.YELLOW));
// TODO: Open BlockEditGui
return true; return true;
} }
case SLOT_RESET -> { case SLOT_RESET -> {
player.sendMessage(Component.text("Открытие редактора сброса... (в разработке)", NamedTextColor.YELLOW)); player.sendMessage(Component.text("Открытие редактора сброса... (в разработке)", NamedTextColor.YELLOW));
// TODO: Open ResetEditGui
return true; return true;
} }
case SLOT_REWARDS -> { case SLOT_REWARDS -> {
player.sendMessage(Component.text("Открытие редактора наград... (в разработке)", NamedTextColor.YELLOW)); player.sendMessage(Component.text("Открытие редактора наград... (в разработке)", NamedTextColor.YELLOW));
// TODO: Open RewardEditGui
return true; return true;
} }
case SLOT_TELEPORT -> { case SLOT_TELEPORT -> {
player.sendMessage(Component.text("Открытие редактора телепорта... (в разработке)", NamedTextColor.YELLOW)); player.sendMessage(Component.text("Открытие редактора телепорта... (в разработке)", NamedTextColor.YELLOW));
// TODO: Open TeleportEditGui
return true; return true;
} }
case SLOT_UI -> { case SLOT_UI -> {
player.sendMessage(Component.text("Открытие редактора интерфейса... (в разработке)", NamedTextColor.YELLOW)); player.sendMessage(Component.text("Открытие редактора интерфейса... (в разработке)", NamedTextColor.YELLOW));
// TODO: Open UIEditGui
return true; return true;
} }
case SLOT_SAVE -> { case SLOT_SAVE -> {
@ -484,16 +314,13 @@ public final class MineEditGui {
return true; return true;
} }
case SLOT_DELETE -> { case SLOT_DELETE -> {
// Open confirmation GUI
ConfirmDeleteGui.open(plugin, player, mineName); ConfirmDeleteGui.open(plugin, player, mineName);
return true; return true;
} }
case SLOT_INFO, SLOT_REGIONS -> { case SLOT_INFO, SLOT_REGIONS -> {
// Info only, no action
return true; return true;
} }
} }
return true;
return true; // Cancel all other clicks
} }
} }

View file

@ -13,10 +13,13 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.block.Action; import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.EquipmentSlot;
/** /**
* Group wand: ЛКМ / ПКМ по блоку — углы; Shift+ПКМ — GUI группы. * Group wand: ЛКМ / ПКМ по блоку — углы; Shift+ПКМ — GUI группы.
* Also starts/stops particle visualization when holding the wand.
*/ */
public final class PlayerInteractListener implements Listener { public final class PlayerInteractListener implements Listener {
@ -54,6 +57,9 @@ public final class PlayerInteractListener implements Listener {
session.setPos1(slot, block.getLocation()); session.setPos1(slot, block.getLocation());
player.sendMessage(Component.text( player.sendMessage(Component.text(
"Угол 1 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN)); "Угол 1 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN));
// Start showing particles when selection is made
plugin.getWandParticleService().startShowingParticles(player);
return; return;
} }
@ -68,6 +74,34 @@ public final class PlayerInteractListener implements Listener {
session.setPos2(slot, block.getLocation()); session.setPos2(slot, block.getLocation());
player.sendMessage(Component.text( player.sendMessage(Component.text(
"Угол 2 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN)); "Угол 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());
}
} }

View file

@ -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<UUID, BukkitRunnable> 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);
}
}
}