diff --git a/src/main/java/de/nexuslobby/NexusLobby.java b/src/main/java/de/nexuslobby/NexusLobby.java index 0843782..1e2615b 100644 --- a/src/main/java/de/nexuslobby/NexusLobby.java +++ b/src/main/java/de/nexuslobby/NexusLobby.java @@ -13,13 +13,16 @@ import de.nexuslobby.modules.settings.LobbySettingsModule; import de.nexuslobby.modules.portal.PortalManager; import de.nexuslobby.modules.portal.PortalCommand; import de.nexuslobby.modules.servers.ServerSwitcherListener; -import de.nexuslobby.modules.servers.ServerChecker; // Hinzugefügt +import de.nexuslobby.modules.servers.ServerChecker; import de.nexuslobby.modules.armorstandtools.*; import de.nexuslobby.modules.gadgets.GadgetModule; import de.nexuslobby.modules.hologram.HologramModule; import de.nexuslobby.modules.mapart.MapArtModule; import de.nexuslobby.modules.intro.IntroModule; import de.nexuslobby.modules.border.BorderModule; +import de.nexuslobby.modules.parkour.ParkourManager; +import de.nexuslobby.modules.parkour.ParkourListener; +import de.nexuslobby.modules.player.PlayerInspectModule; // NEU import de.nexuslobby.utils.*; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import net.md_5.bungee.api.chat.ClickEvent; @@ -38,6 +41,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitRunnable; @@ -62,6 +66,7 @@ public class NexusLobby extends JavaPlugin implements Listener { private MapArtModule mapArtModule; private IntroModule introModule; private BorderModule borderModule; + private ParkourManager parkourManager; private ConversationManager conversationManager; @@ -85,6 +90,10 @@ public class NexusLobby extends JavaPlugin implements Listener { return conversationManager; } + public ParkourManager getParkourManager() { + return parkourManager; + } + @Override public void onEnable() { instance = this; @@ -94,15 +103,17 @@ public class NexusLobby extends JavaPlugin implements Listener { getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord"); moduleManager = new ModuleManager(this); + // --- Parkour & Conversation Initialisierung --- + this.parkourManager = new ParkourManager(this); this.conversationManager = new ConversationManager(this); ArmorStandGUI.init(); registerModules(); moduleManager.enableAll(); + registerListeners(); - // --- STATUS CHECKER START --- ServerChecker.startGlobalChecker(); new ArmorStandLookAtModule(); @@ -124,40 +135,11 @@ public class NexusLobby extends JavaPlugin implements Listener { new BukkitRunnable() { @Override public void run() { - if (conversationManager == null) return; - - for (World world : Bukkit.getWorlds()) { - for (ArmorStand as : world.getEntitiesByClass(ArmorStand.class)) { - if (as.getScoreboardTags().stream().anyMatch(tag -> tag.startsWith("conv_id:"))) { - boolean playerNearby = false; - for (Entity nearby : as.getNearbyEntities(30, 30, 30)) { - if (nearby instanceof Player) { - playerNearby = true; - break; - } - } - - if (playerNearby) { - String dialogId = null; - String partnerUUID = null; - - for (String tag : as.getScoreboardTags()) { - if (tag.startsWith("conv_id:")) dialogId = tag.split(":")[1]; - if (tag.startsWith("conv_partner:")) partnerUUID = tag.split(":")[1]; - } - - if (dialogId != null && partnerUUID != null) { - try { - UUID partnerId = UUID.fromString(partnerUUID); - conversationManager.playConversation(as.getUniqueId(), partnerId, dialogId); - } catch (Exception ignored) {} - } - } - } - } + if (conversationManager != null) { + conversationManager.startAllAutomatedConversations(); } } - }.runTaskTimer(this, 20L * 30, 20L * 90); + }.runTaskTimer(this, 100L, 300L); } public void reloadPlugin() { @@ -250,6 +232,9 @@ public class NexusLobby extends JavaPlugin implements Listener { this.tablistModule = new TablistModule(); moduleManager.registerModule(tablistModule); + // Player Inspect Modul registrieren + moduleManager.registerModule(new PlayerInspectModule()); // NEU + this.portalManager = new PortalManager(this); moduleManager.registerModule(portalManager); } @@ -262,6 +247,24 @@ public class NexusLobby extends JavaPlugin implements Listener { getServer().getPluginManager().registerEvents(new PlayerHider(), this); getServer().getPluginManager().registerEvents(new MaintenanceListener(), this); getServer().getPluginManager().registerEvents(new ASTListener(), this); + getServer().getPluginManager().registerEvents(new ParkourListener(this.parkourManager), this); + + getServer().getPluginManager().registerEvents(new NPCClickListener(), this); + } + + public class NPCClickListener implements Listener { + @EventHandler + public void onNPCClick(PlayerInteractAtEntityEvent event) { + Entity entity = event.getRightClicked(); + Player player = event.getPlayer(); + + if (entity instanceof ArmorStand as) { + if (as.getScoreboardTags().contains("parkour_trainer")) { + player.performCommand("warp parkour"); + player.sendMessage("§6§lTrainer §8» §aViel Erfolg beim Parkour! Gib dein Bestes!"); + } + } + } } @EventHandler(priority = EventPriority.LOWEST) @@ -348,7 +351,9 @@ public class NexusLobby extends JavaPlugin implements Listener { @Override public void onDisable() { getServer().getMessenger().unregisterOutgoingPluginChannel(this, "BungeeCord"); - if (moduleManager != null) moduleManager.disableAll(); + if (moduleManager != null) { + moduleManager.disableAll(); + } getLogger().info("NexusLobby deaktiviert."); } @@ -417,17 +422,22 @@ public class NexusLobby extends JavaPlugin implements Listener { if (params.equalsIgnoreCase("version")) return NexusLobby.this.getDescription().getVersion(); if (params.equalsIgnoreCase("build_mode")) return BuildCommand.isInBuildMode(player) ? "§aAktiv" : "§cInaktiv"; if (params.equalsIgnoreCase("silent_join")) return silentPlayers.contains(player.getUniqueId()) ? "§aEin" : "§cAus"; + if (params.equalsIgnoreCase("parkour_top")) { + return parkourManager != null ? parkourManager.getTopTen() : "N/A"; + } return null; } } public ModuleManager getModuleManager() { return moduleManager; } - public PortalManager getPortalManager() { return portalManager; } // Hinzugefügt/Wiederhergestellt + public PortalManager getPortalManager() { return portalManager; } public TablistModule getTablistModule() { return tablistModule; } public LobbySettingsModule getLobbySettingsModule() { return lobbySettingsModule; } public ItemsModule getItemsModule() { return itemsModule; } public GadgetModule getGadgetModule() { return gadgetModule; } public HologramModule getHologramModule() { return hologramModule; } public DynamicArmorStandModule getDynamicArmorStandModule() { return dynamicArmorStandModule; } - public MapArtModule getMapArtModule() { return mapArtModule; } // Wiederhergestellt + public MapArtModule getMapArtModule() { return mapArtModule; } + public IntroModule getIntroModule() { return introModule; } + public BorderModule getBorderModule() { return borderModule; } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/commands/LobbyTabCompleter.java b/src/main/java/de/nexuslobby/commands/LobbyTabCompleter.java index e0376c9..81143ac 100644 --- a/src/main/java/de/nexuslobby/commands/LobbyTabCompleter.java +++ b/src/main/java/de/nexuslobby/commands/LobbyTabCompleter.java @@ -32,7 +32,7 @@ public class LobbyTabCompleter implements TabCompleter { if (cmdName.equals("nexuslobby") || cmdName.equals("nexus")) { if (args.length == 1) { if (sender.hasPermission("nexuslobby.admin")) { - suggestions.addAll(Arrays.asList("reload", "setspawn", "silentjoin")); + suggestions.addAll(Arrays.asList("reload", "setspawn", "silentjoin", "parkour")); } suggestions.add("sb"); } else if (args.length == 2) { @@ -43,6 +43,12 @@ public class LobbyTabCompleter implements TabCompleter { } } else if (args[0].equalsIgnoreCase("silentjoin")) { suggestions.addAll(Arrays.asList("on", "off")); + } else if (args[0].equalsIgnoreCase("parkour")) { + suggestions.addAll(Arrays.asList("setstart", "setfinish", "setcheckpoint", "reset", "clear", "removeall")); + } + } else if (args.length == 3) { + if (args[0].equalsIgnoreCase("parkour") && args[1].equalsIgnoreCase("setcheckpoint")) { + suggestions.addAll(Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8", "9")); } } } @@ -56,7 +62,7 @@ public class LobbyTabCompleter implements TabCompleter { suggestions.addAll(hologramModule.getHologramIds()); } } - } + } // --- Wartungsmodus --- else if (cmdName.equals("maintenance")) { @@ -74,7 +80,7 @@ public class LobbyTabCompleter implements TabCompleter { suggestions.addAll(portalManager.getPortalNames()); } } - } + } // --- MapArt --- else if (cmdName.equals("mapart")) { @@ -101,10 +107,10 @@ public class LobbyTabCompleter implements TabCompleter { } } - // --- NexusCmd (ArmorStand Commands & Dialoge) --- - else if (cmdName.equals("nexuscmd") || cmdName.equals("ncmd") || cmdName.equals("ascmd")) { + // --- NexusCmd / ArmorStandTools --- + else if (cmdName.equals("nexuscmd") || cmdName.equals("ncmd") || cmdName.equals("ascmd") || cmdName.equals("conv")) { if (args.length == 1) { - suggestions.addAll(Arrays.asList("add", "remove", "list", "name", "lookat", "conv")); + suggestions.addAll(Arrays.asList("add", "remove", "list", "name", "lookat", "conv", "say")); } else if (args.length == 2) { if (args[0].equalsIgnoreCase("add")) { @@ -112,10 +118,12 @@ public class LobbyTabCompleter implements TabCompleter { } else if (args[0].equalsIgnoreCase("name")) { suggestions.addAll(Arrays.asList("", "none")); } else if (args[0].equalsIgnoreCase("conv")) { - // NEU: unlink hinzugefügt - suggestions.addAll(Arrays.asList("select1", "select2", "link", "unlink", "start")); + // ERWEITERT: select3 und select4 hinzugefügt + suggestions.addAll(Arrays.asList("select1", "select2", "select3", "select4", "link", "unlink", "start")); } else if (args[0].equalsIgnoreCase("remove")) { - suggestions.add("all"); + suggestions.addAll(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "all")); + } else if (args[0].equalsIgnoreCase("say")) { + suggestions.add(""); } } else if (args.length == 3) { @@ -139,11 +147,14 @@ public class LobbyTabCompleter implements TabCompleter { } } - // --- ArmorStandTools (/astools) --- + // --- ArmorStandTools Alternate --- else if (cmdName.equals("astools") || cmdName.equals("nt") || cmdName.equals("ntools")) { if (args.length == 1) { - suggestions.addAll(Arrays.asList("dynamic", "lookat", "addplayer", "addconsole", "remove", "reload")); + suggestions.addAll(Arrays.asList("dynamic", "lookat", "addplayer", "addconsole", "remove", "reload", "say")); } + else if (args.length == 2 && args[0].equalsIgnoreCase("say")) { + suggestions.add(""); + } else if (args.length == 2 && (args[0].equalsIgnoreCase("addplayer") || args[0].equalsIgnoreCase("addconsole"))) { suggestions.addAll(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")); } @@ -152,7 +163,7 @@ public class LobbyTabCompleter implements TabCompleter { } } - // Filtert die Liste basierend auf der bisherigen Eingabe (für Case-Insensitivity und Teilübereinstimmung) + // Filtert die Liste basierend auf der bisherigen Eingabe return suggestions.stream() .filter(s -> s.toLowerCase().startsWith(args[args.length - 1].toLowerCase())) .collect(Collectors.toList()); diff --git a/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java b/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java index 5208fba..370a72c 100644 --- a/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java +++ b/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java @@ -2,6 +2,7 @@ package de.nexuslobby.commands; import de.nexuslobby.NexusLobby; import de.nexuslobby.modules.ScoreboardModule; +import de.nexuslobby.modules.parkour.ParkourManager; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Sound; @@ -10,9 +11,13 @@ import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import java.util.List; + public class NexusLobbyCommand implements CommandExecutor { @Override @@ -23,8 +28,29 @@ public class NexusLobbyCommand implements CommandExecutor { return true; } + String cmdName = command.getName().toLowerCase(); + ParkourManager pm = NexusLobby.getInstance().getParkourManager(); + + // --- DIREKTE KURZ-BEFEHLE --- + if (cmdName.equalsIgnoreCase("setstart")) { + if (!player.hasPermission("nexuslobby.admin")) return noPerm(player); + handleSetStart(player, pm); + return true; + } + if (cmdName.equalsIgnoreCase("setcheckpoint")) { + if (!player.hasPermission("nexuslobby.admin")) return noPerm(player); + pm.setCheckpoint(player, player.getLocation()); + return true; + } + if (cmdName.equalsIgnoreCase("setfinish")) { + if (!player.hasPermission("nexuslobby.admin")) return noPerm(player); + pm.setFinishLocation(player.getLocation()); + player.sendMessage("§8[§6Nexus§8] §aParkour-Zielpunkt gesetzt!"); + return true; + } + // --- SPAWN BEFEHL --- - if (command.getName().equalsIgnoreCase("spawn")) { + if (cmdName.equalsIgnoreCase("spawn")) { FileConfiguration config = NexusLobby.getInstance().getConfig(); if (config.contains("spawn.world")) { Location loc = getSpawnFromConfig(config); @@ -41,7 +67,7 @@ public class NexusLobbyCommand implements CommandExecutor { return true; } - // --- HAUPTBEFEHL ARGUMENTE --- + // --- HAUPTBEFEHL /NEXUSLOBBY oder /NEXUS --- if (args.length == 0) { sendInfo(player); return true; @@ -49,23 +75,14 @@ public class NexusLobbyCommand implements CommandExecutor { switch (args[0].toLowerCase()) { case "reload": - if (!player.hasPermission("nexuslobby.admin")) { - player.sendMessage("§cKeine Berechtigung."); - return true; - } - - // Aufruf der Reload-Methode in der Hauptklasse + if (!player.hasPermission("nexuslobby.admin")) return noPerm(player); NexusLobby.getInstance().reloadPlugin(); - player.sendMessage("§8[§6Nexus§8] §aPlugin erfolgreich neu geladen!"); player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1f, 1.5f); break; case "setspawn": - if (!player.hasPermission("nexuslobby.admin")) { - player.sendMessage("§cKeine Berechtigung."); - return true; - } + if (!player.hasPermission("nexuslobby.admin")) return noPerm(player); Location loc = player.getLocation(); FileConfiguration config = NexusLobby.getInstance().getConfig(); config.set("spawn.world", loc.getWorld().getName()); @@ -79,10 +96,7 @@ public class NexusLobbyCommand implements CommandExecutor { break; case "silentjoin": - if (!player.hasPermission("nexuslobby.silentjoin")) { - player.sendMessage("§cKeine Berechtigung."); - return true; - } + if (!player.hasPermission("nexuslobby.silentjoin")) return noPerm(player); if (NexusLobby.getInstance().getSilentPlayers().contains(player.getUniqueId())) { NexusLobby.getInstance().getSilentPlayers().remove(player.getUniqueId()); player.sendMessage("§8[§6Nexus§8] §7Silent Join: §cDeaktiviert"); @@ -96,6 +110,45 @@ public class NexusLobbyCommand implements CommandExecutor { handleScoreboard(player, args); break; + case "parkour": + if (args.length < 2) { + player.sendMessage("§8[§6Nexus§8] §7Nutze: §e/nexus parkour "); + return true; + } + + String sub = args[1].toLowerCase(); + if (!player.hasPermission("nexuslobby.admin") && !sub.equals("reset")) return noPerm(player); + + switch (sub) { + case "setstart": + handleSetStart(player, pm); + break; + case "setfinish": + pm.setFinishLocation(player.getLocation()); + player.sendMessage("§8[§6Nexus§8] §aParkour-Zielpunkt gesetzt!"); + break; + case "setcheckpoint": + pm.setCheckpoint(player, player.getLocation()); + break; + case "reset": + pm.stopParkour(player); + player.sendMessage("§8[§6Nexus§8] §7Dein aktueller Lauf wurde abgebrochen."); + break; + case "clear": + pm.clearStats(); + player.sendMessage("§8[§6Nexus§8] §aAlle Parkour-Bestzeiten wurden gelöscht!"); + break; + case "removeall": + pm.removeAllPoints(); + player.sendMessage("§8[§6Nexus§8] §cDie gesamte Strecke (Checkpoints & Ziel) wurde gelöscht!"); + player.playSound(player.getLocation(), Sound.ENTITY_ITEM_BREAK, 1f, 1f); + break; + default: + player.sendMessage("§cUnbekannter Unterbefehl."); + break; + } + break; + default: sendInfo(player); break; @@ -104,6 +157,34 @@ public class NexusLobbyCommand implements CommandExecutor { return true; } + private void handleSetStart(Player player, ParkourManager pm) { + // NPC Erkennung (Blickrichtung auf ArmorStand) + ArmorStand targetAs = null; + List nearby = player.getNearbyEntities(4, 4, 4); + for (Entity e : nearby) { + if (e instanceof ArmorStand as) { + double dot = player.getLocation().getDirection().dot(as.getLocation().toVector().subtract(player.getLocation().toVector()).normalize()); + if (dot > 0.9) { + targetAs = as; + break; + } + } + } + + if (targetAs != null) { + targetAs.addScoreboardTag("parkour_npc"); + player.sendMessage("§8[§6Nexus§8] §aArmorStand als Parkour-NPC markiert!"); + } + + pm.setStartLocation(player.getLocation()); + player.sendMessage("§8[§6Nexus§8] §aParkour-Startpunkt an deiner Position gesetzt!"); + } + + private boolean noPerm(Player player) { + player.sendMessage("§cKeine Berechtigung."); + return true; + } + private void handleScoreboard(Player player, String[] args) { if (args.length < 2) { player.sendMessage("§cBenutzung: /nexus sb "); @@ -141,11 +222,12 @@ public class NexusLobbyCommand implements CommandExecutor { player.sendMessage("§8§m--------------------------------------"); player.sendMessage("§6§lNexusLobby §7- Informationen"); player.sendMessage(""); - player.sendMessage("§f/spawn §7- Zum Spawn teleportieren"); + player.sendMessage("§f/spawn §7- Zum Spawn"); + player.sendMessage("§f/setstart §8| §f/setcheckpoint §8| §f/setfinish"); + player.sendMessage("§f/nexus parkour removeall §7- Strecke löschen"); player.sendMessage("§f/nexus setspawn §7- Spawn setzen"); - player.sendMessage("§f/nexus silentjoin §7- Join-Nachricht umschalten"); player.sendMessage("§f/nexus sb §7- Scoreboard"); - player.sendMessage("§f/nexus reload §7- Konfiguration laden"); + player.sendMessage("§f/nexus reload §7- Config laden"); player.sendMessage("§8§m--------------------------------------"); } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ASTListener.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ASTListener.java index b70cdcf..25c5c24 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ASTListener.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ASTListener.java @@ -16,6 +16,8 @@ import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import org.bukkit.util.EulerAngle; +import java.util.UUID; + public class ASTListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @@ -35,7 +37,11 @@ public class ASTListener implements Listener { return; } - // --- 2. FALL: NORMALER KLICK -> Befehle ausführen (Bungee, etc.) --- + // --- 2. FALL: NORMALER KLICK -> Dialog manuell triggern --- + // Dies triggert das Gruppensystem im ConversationManager + checkAndTriggerDialog(as, p); + + // --- 3. FALL: Befehle ausführen (Bungee, etc.) --- for (String tag : as.getScoreboardTags()) { if (tag.startsWith("ascmd:")) { String[] parts = tag.split(":"); @@ -44,7 +50,7 @@ public class ASTListener implements Listener { String type = parts[3].toLowerCase(); String command = parts[4]; - event.setCancelled(true); // Verhindert z.B. dass man Items klaut + event.setCancelled(true); switch (type) { case "bungee": @@ -57,7 +63,38 @@ public class ASTListener implements Listener { Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command.replace("%player%", p.getName())); break; } - break; // Nur den ersten gefundenen Befehl ausführen + break; + } + } + } + + /** + * Prüft, ob der ArmorStand Dialog-Tags hat und startet das Gespräch über den Manager. + * Nutzt nun die Gruppen-Logik (z.B. owner_suche). + */ + private void checkAndTriggerDialog(ArmorStand as, Player p) { + String groupName = null; + String partnerUUIDString = null; + + for (String tag : as.getScoreboardTags()) { + // conv_id enthält jetzt den Gruppennamen aus der conversations.yml + if (tag.startsWith("conv_id:")) groupName = tag.split(":")[1]; + if (tag.startsWith("conv_partner:")) partnerUUIDString = tag.split(":")[1]; + } + + if (groupName != null && partnerUUIDString != null) { + try { + UUID partnerUUID = UUID.fromString(partnerUUIDString); + + // Wir rufen playConversation auf. Der Manager entscheidet selbst + // anhand der Uhrzeit, ob er morgens, mittags oder nachts abspielt. + NexusLobby.getInstance().getConversationManager().playConversation( + as.getUniqueId(), + partnerUUID, + groupName + ); + } catch (Exception ignored) { + // Falls die UUID oder Gruppe ungültig ist } } } @@ -97,6 +134,7 @@ public class ASTListener implements Listener { ArmorStandTool tool = ArmorStandTool.get(item); if (tool != null) { tool.execute(as, p); + // Menü aktualisieren, falls wir noch im selben Editor sind if (p.getOpenInventory().getTitle().equals(title)) { new ArmorStandGUI(as, p); } diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCmdExecutor.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCmdExecutor.java index b2ae0e6..1665021 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCmdExecutor.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCmdExecutor.java @@ -15,11 +15,15 @@ import org.bukkit.util.EulerAngle; import org.bukkit.util.RayTraceResult; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; /** * ArmorStandCmdExecutor - Erweiterte Steuerung für ArmorStand-Interaktionen. * Nutzt Raytracing für präzise Auswahl und permanentes Dialog-Linking sowie Status-Backup. + * Erweitert um Unterstützung für Gruppen-Dialoge (bis zu 4 NPCs). + * * Update: Bedrock-Kompatibilität für Namen korrigiert. */ public class ArmorStandCmdExecutor implements CommandExecutor { @@ -45,18 +49,21 @@ public class ArmorStandCmdExecutor implements CommandExecutor { switch (args[1].toLowerCase()) { case "select1": case "select2": + case "select3": + case "select4": ArmorStand target = getTargetArmorStand(p); if (target == null) { p.sendMessage(prefix + "§cDu musst einen ArmorStand direkt anschauen (Fadenkreuz)!"); return true; } - boolean isFirst = args[1].equalsIgnoreCase("select1"); - String metaKey = isFirst ? "conv_npc1" : "conv_npc2"; + // Dynamische Ermittlung des Slots (1, 2, 3 oder 4) + String slotStr = args[1].toLowerCase().replace("select", ""); + String metaKey = "conv_npc" + slotStr; UUID targetUUID = target.getUniqueId(); p.setMetadata(metaKey, new FixedMetadataValue(NexusLobby.getInstance(), targetUUID.toString())); - p.sendMessage(prefix + "§aNPC §e" + (isFirst ? "1" : "2") + " §amarkiert (§7" + targetUUID.toString().substring(0, 8) + "...§a)"); + p.sendMessage(prefix + "§aNPC §e" + slotStr + " §amarkiert (§7" + targetUUID.toString().substring(0, 8) + "...§a)"); p.spawnParticle(Particle.WITCH, target.getLocation().add(0, 1.0, 0), 15, 0.2, 0.2, 0.2, 0.05); break; @@ -66,24 +73,43 @@ public class ArmorStandCmdExecutor implements CommandExecutor { return true; } if (!p.hasMetadata("conv_npc1") || !p.hasMetadata("conv_npc2")) { - p.sendMessage(prefix + "§cBitte markiere erst beide NPCs (select1 & select2)!"); + p.sendMessage(prefix + "§cBitte markiere mindestens die ersten beiden NPCs (select1 & select2)!"); return true; } UUID id1 = UUID.fromString(p.getMetadata("conv_npc1").get(0).asString()); UUID id2 = UUID.fromString(p.getMetadata("conv_npc2").get(0).asString()); + + // Optionale Partner 3 und 4 abrufen falls vorhanden + UUID id3 = p.hasMetadata("conv_npc3") ? UUID.fromString(p.getMetadata("conv_npc3").get(0).asString()) : null; + UUID id4 = p.hasMetadata("conv_npc4") ? UUID.fromString(p.getMetadata("conv_npc4").get(0).asString()) : null; + String dialogId = args[2]; Entity entity1 = Bukkit.getEntity(id1); if (entity1 instanceof ArmorStand as1) { - as1.getScoreboardTags().removeIf(tag -> tag.startsWith("conv_partner:") || tag.startsWith("conv_id:")); + // Vorhandene Tags säubern + as1.getScoreboardTags().removeIf(tag -> tag.startsWith("conv_partner") || tag.startsWith("conv_id:")); + + // Tags für Partner setzen as1.addScoreboardTag("conv_partner:" + id2.toString()); + if (id3 != null) as1.addScoreboardTag("conv_partner2:" + id3.toString()); + if (id4 != null) as1.addScoreboardTag("conv_partner3:" + id4.toString()); + as1.addScoreboardTag("conv_id:" + dialogId); - NexusLobby.getInstance().getConversationManager().saveLink(id1, id2, dialogId); + // Im Manager speichern (Nutzt die erweiterte Methode für Gruppen) + NexusLobby.getInstance().getConversationManager().saveLinkExtended(id1, id2, id3, id4, dialogId); p.sendMessage(prefix + "§a§lDauerhafte Verknüpfung erstellt!"); + p.sendMessage(prefix + "§7Beteiligte NPCs: §e" + (id3 == null ? "2" : (id4 == null ? "3" : "4"))); p.spawnParticle(Particle.HAPPY_VILLAGER, as1.getLocation().add(0, 1.5, 0), 20, 0.4, 0.4, 0.4, 0.1); + + // Metadaten nach dem Linken aufräumen + p.removeMetadata("conv_npc1", NexusLobby.getInstance()); + p.removeMetadata("conv_npc2", NexusLobby.getInstance()); + p.removeMetadata("conv_npc3", NexusLobby.getInstance()); + p.removeMetadata("conv_npc4", NexusLobby.getInstance()); } else { p.sendMessage(prefix + "§cFehler: Sprecher 1 nicht gefunden."); } @@ -96,8 +122,8 @@ public class ArmorStandCmdExecutor implements CommandExecutor { return true; } - // Ingame Tags entfernen - targetUnlink.getScoreboardTags().removeIf(tag -> tag.startsWith("conv_partner:") || tag.startsWith("conv_id:")); + // Ingame Tags entfernen (alle conv_ Tags) + targetUnlink.getScoreboardTags().removeIf(tag -> tag.startsWith("conv_")); // Aus Konfiguration löschen NexusLobby.getInstance().getConversationManager().removeLink(targetUnlink.getUniqueId()); @@ -112,12 +138,19 @@ public class ArmorStandCmdExecutor implements CommandExecutor { return true; } if (!p.hasMetadata("conv_npc1") || !p.hasMetadata("conv_npc2")) { - p.sendMessage(prefix + "§cBitte markiere erst beide NPCs!"); + p.sendMessage(prefix + "§cBitte markiere mindestens zwei NPCs!"); return true; } - UUID s1 = UUID.fromString(p.getMetadata("conv_npc1").get(0).asString()); - UUID s2 = UUID.fromString(p.getMetadata("conv_npc2").get(0).asString()); - NexusLobby.getInstance().getConversationManager().playConversation(s1, s2, args[2]); + + // Liste der Teilnehmer für den Startbefehl erstellen + List participants = new ArrayList<>(); + participants.add(UUID.fromString(p.getMetadata("conv_npc1").get(0).asString())); + participants.add(UUID.fromString(p.getMetadata("conv_npc2").get(0).asString())); + if (p.hasMetadata("conv_npc3")) participants.add(UUID.fromString(p.getMetadata("conv_npc3").get(0).asString())); + if (p.hasMetadata("conv_npc4")) participants.add(UUID.fromString(p.getMetadata("conv_npc4").get(0).asString())); + + // Gespräch über die Gruppen-Methode starten + NexusLobby.getInstance().getConversationManager().playConversationGroup(participants, args[2]); break; default: @@ -126,9 +159,22 @@ public class ArmorStandCmdExecutor implements CommandExecutor { return true; } - // --- STANDARD TOOLS (LOOKAT / NAME / ADD) --- + // --- STANDARD TOOLS (LOOKAT / NAME / ADD / SAY) --- ArmorStand target = getTargetArmorStand(p); + if (args[0].equalsIgnoreCase("say") && args.length >= 2) { + if (target == null) { p.sendMessage(prefix + "§cSchau einen ArmorStand an!"); return true; } + + String text = buildString(args, 1); + String colored = ChatColor.translateAlternateColorCodes('&', text); + + // Nutzt die showBubble-Logik aus dem ConversationManager (Ohne Partner-Zwang) + NexusLobby.getInstance().getConversationManager().showBubble(target, colored); + + p.sendMessage(prefix + "§aNPC-Sprechblase gesendet."); + return true; + } + if (args[0].equalsIgnoreCase("lookat")) { if (target == null) { p.sendMessage(prefix + "§cSchau einen ArmorStand an!"); return true; } if (target.getScoreboardTags().contains("as_lookat")) { @@ -146,18 +192,21 @@ public class ArmorStandCmdExecutor implements CommandExecutor { if (target == null) { p.sendMessage(prefix + "§cSchau einen ArmorStand an!"); return true; } String nameInput = buildString(args, 1); + // Wichtig: Alle alten Namens-Tags entfernen für Konsistenz + target.getScoreboardTags().removeIf(tag -> tag.startsWith("asname:") || tag.startsWith("as_displayname:")); + if (nameInput.equalsIgnoreCase("none")) { target.setCustomName(""); target.setCustomNameVisible(false); - target.getScoreboardTags().removeIf(tag -> tag.startsWith("asname:")); p.sendMessage(prefix + "§eName entfernt."); } else { String colored = ChatColor.translateAlternateColorCodes('&', nameInput); target.setCustomName(colored); target.setCustomNameVisible(true); - target.getScoreboardTags().removeIf(tag -> tag.startsWith("asname:")); - target.addScoreboardTag("asname:" + nameInput); + // Wir speichern es unter as_displayname, damit das StatusModule es findet + // ":" wird durch "§§" ersetzt, um Probleme in Scoreboard-Tags zu vermeiden + target.addScoreboardTag("as_displayname:" + nameInput.replace(":", "§§")); p.sendMessage(prefix + "§7Name gesetzt: " + colored); p.sendMessage(prefix + "§8(Status-Backup wurde gespeichert)"); @@ -218,8 +267,7 @@ public class ArmorStandCmdExecutor implements CommandExecutor { private boolean sendConvHelp(Player p) { p.sendMessage(" "); p.sendMessage("§6§lConversation Setup:"); - p.sendMessage("§e/nexuscmd conv select1 §7- Sprecher 1"); - p.sendMessage("§e/nexuscmd conv select2 §7- Sprecher 2"); + p.sendMessage("§e/nexuscmd conv select1-4 §7- NPCs markieren"); p.sendMessage("§e/nexuscmd conv link §7- Speichern"); p.sendMessage("§e/nexuscmd conv unlink §7- Verknüpfung lösen"); p.sendMessage("§e/nexuscmd conv start §7- Testen"); @@ -229,7 +277,8 @@ public class ArmorStandCmdExecutor implements CommandExecutor { private boolean sendHelp(Player p) { p.sendMessage("§6§lNexus Tools Hilfe:"); - p.sendMessage("§e/nexuscmd name §7- Setzt Namen & Status-Backup"); + p.sendMessage("§e/nexuscmd name §7- Setzt Namen (Bedrock-Safe)"); + p.sendMessage("§e/nexuscmd say §7- Erstellt eine Sprechblase"); p.sendMessage("§e/nexuscmd lookat §7- Blickkontakt umschalten"); p.sendMessage("§e/nexuscmd add bungee §7- Bungee-Bindung"); p.sendMessage("§e/nexuscmd conv §7- Gesprächs-Menü"); diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandStatusModule.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandStatusModule.java index 1236388..5668249 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandStatusModule.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandStatusModule.java @@ -9,8 +9,24 @@ import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Entity; import org.bukkit.World; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * ArmorStandStatusModule - Optimierte Statusanzeige für Java & Bedrock. + * - Schaltet zwischen Servername und "Offline" um. + * - Anti-Blink-Logik für Java-Spieler. + * - Force-Refresh-Logik für Bedrock-Spieler (Geyser). + */ public class ArmorStandStatusModule implements Module { + private final Map lastStatus = new HashMap<>(); + private final Map failCount = new HashMap<>(); + + // Zähler für den Force-Refresh (Bedrock-Sicherheit) + private int refreshTicks = 0; + @Override public String getName() { return "ArmorStandStatus"; @@ -18,53 +34,92 @@ public class ArmorStandStatusModule implements Module { @Override public void onEnable() { - // Alle 10 Sekunden prüfen - Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), this::updateAllServerArmorStands, 100L, 200L); + // Alle 10 Sekunden (200 Ticks) + Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), () -> { + refreshTicks++; + updateAllServerArmorStands(); + }, 100L, 200L); } @Override - public void onDisable() {} + public void onDisable() { + lastStatus.clear(); + failCount.clear(); + } private void updateAllServerArmorStands() { + // Alle 6 Durchgänge (ca. jede Minute) erzwingen wir ein Update für Bedrock + boolean forceRefresh = (refreshTicks >= 6); + if (forceRefresh) refreshTicks = 0; + for (World world : Bukkit.getWorlds()) { for (Entity entity : world.getEntitiesByClass(ArmorStand.class)) { if (entity instanceof ArmorStand as) { - checkAndRefreshStatus(as); + checkAndRefreshStatus(as, forceRefresh); } } } } - private void checkAndRefreshStatus(ArmorStand as) { - String bungeeTag = null; + private void checkAndRefreshStatus(ArmorStand as, boolean forceRefresh) { + String serverName = null; + for (String tag : as.getScoreboardTags()) { - if (tag.startsWith("ascmd:bungee:")) { - bungeeTag = tag.replace("ascmd:bungee:", ""); - break; + if (tag.startsWith("ascmd:")) { + if (tag.contains(":bungee:")) { + String[] parts = tag.split(":"); + if (parts.length >= 5) { + serverName = parts[4].toLowerCase(); + } else if (tag.startsWith("ascmd:bungee:")) { + serverName = tag.replace("ascmd:bungee:", "").toLowerCase(); + } + } + if (serverName != null) break; } } - if (bungeeTag == null) return; + if (serverName == null) return; - String serverName = bungeeTag.toLowerCase(); + final String finalServerName = serverName; String ip = NexusLobby.getInstance().getConfig().getString("servers." + serverName + ".ip", "127.0.0.1"); int port = NexusLobby.getInstance().getConfig().getInt("servers." + serverName + ".port", 25565); ServerChecker.isOnline(ip, port).thenAccept(isOnline -> { Bukkit.getScheduler().runTask(NexusLobby.getInstance(), () -> { + + // Toleranz-Logik gegen kurzes Flackern + if (isOnline) { + failCount.put(finalServerName, 0); + } else { + int fails = failCount.getOrDefault(finalServerName, 0) + 1; + failCount.put(finalServerName, fails); + if (fails < 2) return; + } + String originalDisplayName = getOriginalName(as); if (originalDisplayName == null) return; - String translatedName = ChatColor.translateAlternateColorCodes('&', originalDisplayName); + // Status-Check: Hat sich etwas geändert? + Boolean lastKnown = lastStatus.get(as.getUniqueId()); + + // Nur wenn Status neu ODER Force-Refresh (für Bedrock) aktiv ist + if (!forceRefresh && lastKnown != null && lastKnown == isOnline) { + return; + } + // Namen setzen if (isOnline) { - // Zeigt nur den normalen Namen an, wenn online + String translatedName = ChatColor.translateAlternateColorCodes('&', originalDisplayName); as.setCustomName(translatedName); } else { - // Zeigt den Namen an und darunter (getrennt durch Leerzeichen/Format) den Status - // Da Minecraft Namen meist einzeilig sind, nutzen wir eine klare farbliche Trennung - as.setCustomName(translatedName + " §7- §cOffline"); + as.setCustomName("§cOffline"); } + + // Bedrock-Fix: Sichtbarkeit explizit triggern + as.setCustomNameVisible(true); + + // Status speichern + lastStatus.put(as.getUniqueId(), isOnline); }); }); } diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java index e409df9..d1f8912 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java @@ -1,9 +1,8 @@ package de.nexuslobby.modules.armorstandtools; import de.nexuslobby.NexusLobby; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Sound; +import org.bukkit.*; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.ArmorStand; @@ -13,99 +12,154 @@ import org.bukkit.scheduler.BukkitRunnable; import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; +import java.util.*; +import java.util.stream.Collectors; public class ConversationManager { private final NexusLobby plugin; private File file; private FileConfiguration config; + + // Verhindert Mehrfach-Gespräche und regelt die 60s Pause + private final Set activeSpeakers = new HashSet<>(); public ConversationManager(NexusLobby plugin) { this.plugin = plugin; setupFile(); + clearHangingBubbles(); + } + + /** + * Gibt alle verfügbaren IDs zurück. + */ + public List getConversationIds() { + if (config == null || !config.contains("conversations")) { + return new ArrayList<>(); + } + ConfigurationSection section = config.getConfigurationSection("conversations"); + if (section == null) return new ArrayList<>(); + + return new ArrayList<>(section.getKeys(false)); + } + + /** + * Entfernt alle verwaisten Sprechblasen in allen Welten. + */ + public void clearHangingBubbles() { + int count = 0; + for (World world : Bukkit.getWorlds()) { + for (ArmorStand as : world.getEntitiesByClass(ArmorStand.class)) { + if (as.getScoreboardTags().contains("nexus_bubble") || + (!as.isVisible() && as.isMarker() && as.getCustomName() != null)) { + as.remove(); + count++; + } + } + } + if (count > 0) { + Bukkit.getLogger().info("[NexusLobby] Cleanup: " + count + " verwaiste Sprechblasen entfernt."); + } } public void setupFile() { this.file = new File(plugin.getDataFolder(), "conversations.yml"); - if (!file.exists()) { - try { - if (!plugin.getDataFolder().exists()) { - plugin.getDataFolder().mkdirs(); - } - plugin.saveResource("conversations.yml", false); - } catch (Exception e) { - try { - file.createNewFile(); - YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration(file); - defaultConfig.set("conversations.test.dialogue", Arrays.asList( - "&eNPC 1: &7Hallo!", - "&aNPC 2: &7Hi, wie geht es dir?", - "&eNPC 1: &7Bestens, danke!" - )); - defaultConfig.save(file); - } catch (IOException ex) { - ex.printStackTrace(); - } - } + plugin.saveResource("conversations.yml", false); } this.config = YamlConfiguration.loadConfiguration(file); } - public void saveLink(UUID id1, UUID id2, String dialogId) { - config.set("links." + id1.toString() + ".partner", id2.toString()); - config.set("links." + id1.toString() + ".dialog", dialogId); // WICHTIG: ".dialog" - saveConfig(); + /** + * Ermittelt die Tageszeit für die YAML-Pfade. + */ + private String getDaytimeSuffix() { + if (Bukkit.getWorlds().isEmpty()) return "mittags"; + World world = Bukkit.getWorlds().get(0); + long time = world.getTime(); + + if (time >= 0 && time < 12000) return "morgens"; + if (time >= 12000 && time < 13000) return "abends"; + if (time >= 13000 && time < 23000) return "nacht"; + return "mittags"; } - public void removeLink(UUID id) { - if (config.contains("links." + id.toString())) { - config.set("links." + id.toString(), null); - saveConfig(); - } + /** + * Pool-Logik für zufällige Dialog-Varianten oder zeitbasierte IDs. + */ + private String resolveDialogKey(String key) { + ConfigurationSection section = config.getConfigurationSection("conversations"); + if (section == null) return key; + + String daytimeKey = key + "_" + getDaytimeSuffix(); + if (section.contains(daytimeKey)) return daytimeKey; + + List pool = section.getKeys(false).stream() + .filter(s -> s.startsWith(key)) + .collect(Collectors.toList()); + + if (pool.isEmpty()) return key; + + return pool.get(new Random().nextInt(pool.size())); } + /** + * Automatische Prüfung für NPCs in Spieler-Nähe (Unterstützt bis zu 4 Partner). + */ public void startAllAutomatedConversations() { - if (config.getConfigurationSection("links") == null) return; + ConfigurationSection links = config.getConfigurationSection("links"); + if (links == null) return; - for (String npc1String : config.getConfigurationSection("links").getKeys(false)) { + for (String npc1String : links.getKeys(false)) { try { UUID id1 = UUID.fromString(npc1String); - UUID id2 = UUID.fromString(config.getString("links." + npc1String + ".partner")); - String dialogId = config.getString("links." + npc1String + ".dialog"); // WICHTIG: ".dialog" + if (activeSpeakers.contains(id1)) continue; - Entity e1 = Bukkit.getEntity(id1); - Entity e2 = Bukkit.getEntity(id2); + ConfigurationSection npcLink = links.getConfigurationSection(npc1String); + if (npcLink == null) continue; - if (e1 instanceof ArmorStand as1 && e2 instanceof ArmorStand as2) { + String dialogId = npcLink.getString("dialog"); + List partners = new ArrayList<>(); + partners.add(id1); // Der Starter ist immer Teilnehmer 0 + + // Partner 1 bis 3 einsammeln (Max 4 Teilnehmer insgesamt) + if (npcLink.contains("partner")) partners.add(UUID.fromString(npcLink.getString("partner"))); + if (npcLink.contains("partner2")) partners.add(UUID.fromString(npcLink.getString("partner2"))); + if (npcLink.contains("partner3")) partners.add(UUID.fromString(npcLink.getString("partner3"))); + + Entity starter = Bukkit.getEntity(id1); + if (starter instanceof ArmorStand as1) { if (as1.getNearbyEntities(15, 15, 15).stream().anyMatch(e -> e instanceof Player)) { - playConversation(id1, id2, dialogId); + String finalDialogId = resolveDialogKey(dialogId); + playConversationGroup(partners, finalDialogId); } } } catch (Exception ignored) {} } } - public List getConversationIds() { - if (config == null || !config.contains("conversations")) { - return new ArrayList<>(); + /** + * Neue Gruppen-Logik: Spielt Dialoge für 2, 3 oder 4 Teilnehmer ab. + */ + public void playConversationGroup(List participants, String key) { + String daytime = getDaytimeSuffix(); + + // Pfad-Ermittlung + String path = "conversations." + key + "." + daytime; + if (!config.contains(path + ".dialogue")) { + path = "conversations." + key + ".mittags"; } - return new ArrayList<>(config.getConfigurationSection("conversations").getKeys(false)); - } - - public void playConversation(UUID id1, UUID id2, String key) { - // Sicherstellen, dass wir auf "conversations.KEY.dialogue" prüfen - if (config == null || !config.contains("conversations." + key)) { - Bukkit.getLogger().warning("[NexusLobby] Dialog-ID '" + key + "' nicht in conversations.yml gefunden!"); - return; + if (!config.contains(path + ".dialogue")) { + path = "conversations." + key; } - List lines = config.getStringList("conversations." + key + ".dialogue"); - if (lines == null || lines.isEmpty()) return; + if (!config.contains(path + ".dialogue")) return; + + List lines = config.getStringList(path + ".dialogue"); + if (lines.isEmpty()) return; + + // Alle Teilnehmer blockieren + for (UUID id : participants) activeSpeakers.add(id); new BukkitRunnable() { int step = 0; @@ -113,29 +167,58 @@ public class ConversationManager { @Override public void run() { if (step >= lines.size()) { + // 60 SEKUNDEN COOLDOWN + Bukkit.getScheduler().runTaskLater(plugin, () -> { + for (UUID id : participants) activeSpeakers.remove(id); + }, 1200L); + this.cancel(); return; } - UUID speakerUUID = (step % 2 == 0) ? id1 : id2; + // Rotations-Logik: Teilnehmer 0 -> 1 -> 2 -> 3 -> 0 ... + UUID speakerUUID = participants.get(step % participants.size()); Entity entity = Bukkit.getEntity(speakerUUID); if (entity instanceof ArmorStand as) { - as.getWorld().playSound(as.getLocation(), Sound.ENTITY_CHICKEN_EGG, 0.5f, 1.5f); + playDynamicSound(as); showBubble(as, lines.get(step)); } else { + for (UUID id : participants) activeSpeakers.remove(id); this.cancel(); return; } - step++; } - }.runTaskTimer(plugin, 0L, 70L); + }.runTaskTimer(plugin, 0L, 90L); // 4.5s Intervall } - private void showBubble(ArmorStand as, String text) { - Location loc = as.getEyeLocation().add(0, 0.6, 0); + /** + * Spielt einen Sound ab, dessen Pitch (Tonhöhe) zum Namen des ArmorStands passt. + */ + private void playDynamicSound(ArmorStand as) { + float pitch = 1.0f; + String name = as.getCustomName() != null ? as.getCustomName() : ""; + if (name.contains("Timmy") || name.contains("Mia") || name.contains("Lotte")) { + pitch = 1.5f + (new Random().nextFloat() * 0.3f); // Hoch (Kind/Frau) + } else if (name.contains("Schmidt") || name.contains("Vater") || name.contains("Trainer")) { + pitch = 0.6f + (new Random().nextFloat() * 0.2f); // Tief (Mann) + } else { + pitch = 0.9f + (new Random().nextFloat() * 0.4f); // Neutral + } + as.getWorld().playSound(as.getLocation(), Sound.ENTITY_VILLAGER_AMBIENT, 0.5f, pitch); + } + + /** + * Erzeugt die Sprechblase und einen visuellen Effekt (Partikel). + */ + public void showBubble(ArmorStand as, String text) { + Location loc = as.getEyeLocation().add(0, 0.8, 0); + + // Kleiner Partikel-Effekt ("Sprechwolke") + as.getWorld().spawnParticle(Particle.CLOUD, loc, 3, 0.1, 0.1, 0.1, 0.02); + ArmorStand bubble = as.getWorld().spawn(loc, ArmorStand.class, s -> { s.setMarker(true); s.setVisible(false); @@ -143,24 +226,51 @@ public class ConversationManager { s.setCustomName(ChatColorTranslate(text)); s.setCustomNameVisible(true); s.setInvulnerable(true); + s.addScoreboardTag("nexus_bubble"); }); - Bukkit.getScheduler().runTaskLater(plugin, bubble::remove, 60L); + Bukkit.getScheduler().runTaskLater(plugin, bubble::remove, 80L); // 4s Sichtbarkeit + } + + // --- Legacy Support & Config Methoden --- + + public void playConversation(UUID id1, UUID id2, String key) { + playConversationGroup(Arrays.asList(id1, id2), key); + } + + /** + * Neue erweiterte Speicher-Methode für bis zu 4 Partner. + */ + public void saveLinkExtended(UUID id1, UUID id2, UUID id3, UUID id4, String dialogId) { + String path = "links." + id1.toString(); + config.set(path + ".dialog", dialogId); + config.set(path + ".partner", id2.toString()); + if (id3 != null) config.set(path + ".partner2", id3.toString()); + if (id4 != null) config.set(path + ".partner3", id4.toString()); + saveConfig(); + } + + public void saveLink(UUID id1, UUID id2, String dialogId) { + saveLinkExtended(id1, id2, null, null, dialogId); + } + + public void removeLink(UUID id) { + config.set("links." + id.toString(), null); + saveConfig(); } private String ChatColorTranslate(String text) { + if (text == null) return ""; return text.replace("&", "§"); } private void saveConfig() { - try { - config.save(file); - } catch (IOException e) { - e.printStackTrace(); - } + try { config.save(file); } catch (IOException e) { e.printStackTrace(); } } public void reload() { - this.config = YamlConfiguration.loadConfiguration(file); + setupFile(); + activeSpeakers.clear(); + clearHangingBubbles(); } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/gadgets/FreezeRay.java b/src/main/java/de/nexuslobby/modules/gadgets/FreezeRay.java new file mode 100644 index 0000000..d3a284b --- /dev/null +++ b/src/main/java/de/nexuslobby/modules/gadgets/FreezeRay.java @@ -0,0 +1,97 @@ +package de.nexuslobby.modules.gadgets; + +import de.nexuslobby.NexusLobby; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class FreezeRay { + // Auf public gesetzt, damit das GadgetModule im PlayerMoveEvent darauf prüfen kann + public static final Set frozenPlayers = new HashSet<>(); + + public static void shoot(Player shooter) { + Location start = shooter.getEyeLocation(); + Vector direction = start.getDirection(); + + // Sound beim Schießen + shooter.getWorld().playSound(start, Sound.ENTITY_SNOW_GOLEM_SHOOT, 1.0f, 1.5f); + + // Wir prüfen in 0.3er Schritten bis zu 15 Blöcke weit + for (double d = 0; d < 15; d += 0.3) { + Location point = start.clone().add(direction.clone().multiply(d)); + + // Partikel-Strahl sichtbar machen + point.getWorld().spawnParticle(Particle.SNOWFLAKE, point, 1, 0, 0, 0, 0); + + // Prüfung auf Spieler im Umkreis von 0.8 Blöcken (etwas großzügiger) + for (Entity entity : point.getWorld().getNearbyEntities(point, 0.8, 0.8, 0.8)) { + if (entity instanceof Player target && target != shooter) { + applyFreeze(target); + return; // Stoppt den Strahl beim ersten Treffer + } + } + + // Stoppe den Strahl, falls er eine Wand trifft + if (point.getBlock().getType().isSolid()) { + break; + } + } + } + + private static void applyFreeze(Player target) { + if (frozenPlayers.contains(target.getUniqueId())) return; + + frozenPlayers.add(target.getUniqueId()); + + // Fixiere die Position für den Stasis-Effekt + final Location freezeLocation = target.getLocation(); + + // Feedback für getroffenen Spieler + target.sendMessage("§8[§6Nexus§8] §bDu wurdest eingefroren!"); + target.getWorld().playSound(target.getLocation(), Sound.BLOCK_GLASS_BREAK, 1.0f, 0.5f); + + new org.bukkit.scheduler.BukkitRunnable() { + int ticks = 0; + @Override + public void run() { + // Sicherheitscheck: Ist der Spieler noch online? + if (!target.isOnline() || ticks >= 60) { + frozenPlayers.remove(target.getUniqueId()); + this.cancel(); + return; + } + + // Stasis-Effekt: Bewegung auf 0 setzen und Position fixieren + target.setVelocity(new Vector(0, 0, 0)); + + // Alle 2 Ticks zurückteleportieren, falls er versucht zu laufen + // (Behält die Blickrichtung des Spielers bei) + Location current = target.getLocation(); + if (current.getX() != freezeLocation.getX() || current.getZ() != freezeLocation.getZ()) { + freezeLocation.setYaw(current.getYaw()); + freezeLocation.setPitch(current.getPitch()); + target.teleport(freezeLocation); + } + + // Optischer Käfig (Ring-Effekt) + Location loc = target.getLocation(); + for (int i = 0; i < 8; i++) { + double angle = i * Math.PI / 4; + double x = Math.cos(angle) * 0.7; + double z = Math.sin(angle) * 0.7; + loc.getWorld().spawnParticle(Particle.SNOWFLAKE, loc.clone().add(x, 1, z), 1, 0, 0, 0, 0); + loc.getWorld().spawnParticle(Particle.SNOWFLAKE, loc.clone().add(x, 0.2, z), 1, 0, 0, 0, 0); + } + + ticks += 2; + } + }.runTaskTimer(NexusLobby.getInstance(), 0L, 2L); + } +} \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java b/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java index 831c5d3..f213fa1 100644 --- a/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java +++ b/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java @@ -9,8 +9,11 @@ import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.PlayerFishEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; @@ -56,6 +59,37 @@ public class GadgetModule implements Module, Listener { }, 1L, 1L); } + // Event für die Benutzung der aktiven Gadgets + @EventHandler + public void onInteract(PlayerInteractEvent event) { + ItemStack item = event.getItem(); + if (item == null || !item.hasItemMeta()) return; + String name = item.getItemMeta().getDisplayName(); + + if (event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK) { + if (name.equals("§b§lFreeze-Ray")) { + FreezeRay.shoot(event.getPlayer()); + event.setCancelled(true); + } else if (name.equals("§6§lPaintball-Gun")) { + PaintballGun.shoot(event.getPlayer()); + event.setCancelled(true); + } else if (name.equals("§c§lMeteorit")) { + MeteorStrike.launch(event.getPlayer()); + event.setCancelled(true); + } + } + } + + // Verhindert Bewegung für eingefrorene Spieler (Stasis) + @EventHandler + public void onPlayerMove(PlayerMoveEvent event) { + if (FreezeRay.frozenPlayers.contains(event.getPlayer().getUniqueId())) { + if (event.getFrom().getX() != event.getTo().getX() || event.getFrom().getZ() != event.getTo().getZ()) { + event.setTo(event.getFrom().setDirection(event.getTo().getDirection())); + } + } + } + private void handleSpecialHatEffects(Player p) { ItemStack hat = p.getInventory().getHelmet(); if (hat == null || hat.getType() == Material.AIR) return; @@ -162,9 +196,12 @@ public class GadgetModule implements Module, Listener { private void openFunGUI(Player player) { Inventory gui = Bukkit.createInventory(null, 27, FUN_TITLE); fillEdges(gui); - gui.setItem(11, createItem(Material.FISHING_ROD, "§b§lEnterhaken", "§7Zieh dich durch die Luft!")); - gui.setItem(13, createItem(Material.SHIELD, "§5§lSchutzzone", "§7Halte andere auf Distanz")); - gui.setItem(15, createItem(Material.EGG, "§f§lChicken-Rain", "§7Gack-Gack! Hühner überall!")); + gui.setItem(10, createItem(Material.FISHING_ROD, "§b§lEnterhaken", "§7Zieh dich durch die Luft!")); + gui.setItem(11, createItem(Material.PACKED_ICE, "§b§lFreeze-Ray", "§7Friere andere Spieler ein!")); + gui.setItem(12, createItem(Material.GOLDEN_HOE, "§6§lPaintball-Gun", "§7Male die Lobby bunt aus!")); + gui.setItem(14, createItem(Material.FIRE_CHARGE, "§c§lMeteorit", "§7Lass es krachen!")); + gui.setItem(15, createItem(Material.SHIELD, "§5§lSchutzzone", "§7Halte andere auf Distanz")); + gui.setItem(16, createItem(Material.EGG, "§f§lChicken-Rain", "§7Gack-Gack! Hühner überall!")); gui.setItem(22, createItem(Material.ARROW, "§7Zurück", "§8Zum Hauptmenü")); player.openInventory(gui); } @@ -225,6 +262,15 @@ public class GadgetModule implements Module, Listener { } else if (item.getType() == Material.FISHING_ROD) { player.getInventory().addItem(createItem(Material.FISHING_ROD, "§b§lEnterhaken", "§7Rechtsklick zum Katapultieren")); player.closeInventory(); + } else if (item.getType() == Material.PACKED_ICE) { + player.getInventory().addItem(createItem(Material.PACKED_ICE, "§b§lFreeze-Ray", "§7Rechtsklick zum Einfrieren")); + player.closeInventory(); + } else if (item.getType() == Material.GOLDEN_HOE) { + player.getInventory().addItem(createItem(Material.GOLDEN_HOE, "§6§lPaintball-Gun", "§7Rechtsklick zum Schießen")); + player.closeInventory(); + } else if (item.getType() == Material.FIRE_CHARGE) { + player.getInventory().addItem(createItem(Material.FIRE_CHARGE, "§c§lMeteorit", "§7Rechtsklick zum Markieren")); + player.closeInventory(); } else if (item.getType() == Material.SHIELD) { if (activeShields.contains(player.getUniqueId())) { activeShields.remove(player.getUniqueId()); @@ -261,6 +307,9 @@ public class GadgetModule implements Module, Listener { PetManager.removePet(player); HatManager.removeHat(player); player.getInventory().remove(Material.FISHING_ROD); + player.getInventory().remove(Material.PACKED_ICE); + player.getInventory().remove(Material.GOLDEN_HOE); + player.getInventory().remove(Material.FIRE_CHARGE); player.sendMessage("§8[§6Nexus§8] §cAlle Gadgets abgelegt."); } diff --git a/src/main/java/de/nexuslobby/modules/gadgets/MeteorStrike.java b/src/main/java/de/nexuslobby/modules/gadgets/MeteorStrike.java new file mode 100644 index 0000000..98fd69a --- /dev/null +++ b/src/main/java/de/nexuslobby/modules/gadgets/MeteorStrike.java @@ -0,0 +1,47 @@ +package de.nexuslobby.modules.gadgets; + +import de.nexuslobby.NexusLobby; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +public class MeteorStrike { + + public static void launch(Player shooter) { + Location target = null; + Location start = shooter.getEyeLocation(); + Vector direction = start.getDirection(); + + for (double d = 0; d < 30; d += 0.5) { + Location point = start.clone().add(direction.clone().multiply(d)); + if (point.getBlock().getType().isSolid()) { + target = point; + break; + } + } + + if (target == null) return; + final Location finalTarget = target.clone().add(0, 0.5, 0); + + finalTarget.getWorld().spawnParticle(Particle.FLAME, finalTarget, 20, 0.5, 0.1, 0.5, 0.05); + shooter.sendMessage("§8[§6Nexus§8] §cMeteorit im Anflug..."); + + org.bukkit.Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { + // EXPLOSION_EMITTER ist der moderne Name für HUGE_EXPLOSION + finalTarget.getWorld().spawnParticle(Particle.EXPLOSION_EMITTER, finalTarget, 1); + finalTarget.getWorld().spawnParticle(Particle.LAVA, finalTarget, 30, 0.5, 0.5, 0.5, 0.1); + finalTarget.getWorld().playSound(finalTarget, Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 0.8f); + + for (Entity entity : finalTarget.getWorld().getNearbyEntities(finalTarget, 4, 4, 4)) { + if (entity instanceof Player p) { + Vector v = p.getLocation().toVector().subtract(finalTarget.toVector()).normalize().multiply(1.5).setY(0.5); + p.setVelocity(v); + p.sendMessage("§cBUMM!"); + } + } + }, 30L); + } +} \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/gadgets/PaintballGun.java b/src/main/java/de/nexuslobby/modules/gadgets/PaintballGun.java new file mode 100644 index 0000000..3355c02 --- /dev/null +++ b/src/main/java/de/nexuslobby/modules/gadgets/PaintballGun.java @@ -0,0 +1,89 @@ +package de.nexuslobby.modules.gadgets; + +import de.nexuslobby.NexusLobby; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.util.Random; + +public class PaintballGun { + private static final Random random = new Random(); + + // Wir nutzen jetzt Wolle für kräftigere Farben + private static final Material[] COLORS = { + Material.RED_WOOL, Material.BLUE_WOOL, Material.LIME_WOOL, + Material.ORANGE_WOOL, Material.MAGENTA_WOOL, Material.LIGHT_BLUE_WOOL, + Material.YELLOW_WOOL, Material.PURPLE_WOOL, Material.PINK_WOOL + }; + + public static void shoot(Player shooter) { + Location start = shooter.getEyeLocation(); + Vector direction = start.getDirection(); + Material randomColor = COLORS[random.nextInt(COLORS.length)]; + + shooter.getWorld().playSound(start, Sound.ENTITY_CHICKEN_EGG, 1.0f, 2.0f); + + for (double d = 0; d < 25; d += 0.5) { + Location point = start.clone().add(direction.clone().multiply(d)); + + // Flug-Partikel (kleiner Rauch oder Schneeball) + point.getWorld().spawnParticle(Particle.ITEM_SNOWBALL, point, 1, 0, 0, 0, 0); + + Block block = point.getBlock(); + if (block.getType().isSolid()) { + impact(block, randomColor); + break; + } + } + } + + private static void impact(Block centerBlock, Material color) { + Location centerLoc = centerBlock.getLocation(); + centerLoc.getWorld().playSound(centerLoc, Sound.ENTITY_SLIME_SQUISH, 1.0f, 1.2f); + + int radius = 2; // Radius der Farbkugel + + // Wir gehen alle Blöcke im Würfel um den Einschlag durch + for (int x = -radius; x <= radius; x++) { + for (int y = -radius; y <= radius; y++) { + for (int z = -radius; z <= radius; z++) { + + // Berechne Distanz für eine Kugelform (statt Würfel) + if (x * x + y * y + z * z <= radius * radius + 0.5) { + Block target = centerLoc.clone().add(x, y, z).getBlock(); + + // Nur solide Blöcke färben (keine Luft/Gras) + if (target.getType().isSolid()) { + applyColor(target, color); + } + } + } + } + } + } + + private static void applyColor(Block block, Material color) { + Location loc = block.getLocation(); + + // Effekt-Partikel am Block + loc.getWorld().spawnParticle(Particle.BLOCK, loc.clone().add(0.5, 0.5, 0.5), 3, 0.1, 0.1, 0.1, color.createBlockData()); + + // Block-Änderung an alle senden + for (Player online : Bukkit.getOnlinePlayers()) { + online.sendBlockChange(loc, color.createBlockData()); + } + + // Nach 10 Sekunden zurücksetzen + Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { + for (Player online : Bukkit.getOnlinePlayers()) { + online.sendBlockChange(loc, block.getBlockData()); + } + }, 400L); + } +} \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/parkour/ParkourListener.java b/src/main/java/de/nexuslobby/modules/parkour/ParkourListener.java new file mode 100644 index 0000000..7b259fd --- /dev/null +++ b/src/main/java/de/nexuslobby/modules/parkour/ParkourListener.java @@ -0,0 +1,113 @@ +package de.nexuslobby.modules.parkour; + +import de.nexuslobby.NexusLobby; +import org.bukkit.Location; +import org.bukkit.Sound; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +public class ParkourListener implements Listener { + + private final ParkourManager manager; + private final String NPC_TAG = "parkour_npc"; + private final HashMap startCooldown = new HashMap<>(); + + public ParkourListener(ParkourManager manager) { + this.manager = manager; + } + + /** + * Startet den Parkour per Rechtsklick auf den ArmorStand + */ + @EventHandler + public void onInteract(PlayerInteractAtEntityEvent event) { + if (!(event.getRightClicked() instanceof ArmorStand as)) return; + + // Prüfen, ob der ArmorStand den richtigen Tag hat + if (as.getScoreboardTags().contains(NPC_TAG)) { + Player player = event.getPlayer(); + + // Abbrechen, wenn der Spieler schon im Parkour ist + if (manager.isIngame(player)) return; + + // Cooldown-Check (3 Sekunden) + if (startCooldown.containsKey(player.getUniqueId())) { + if (System.currentTimeMillis() - startCooldown.get(player.getUniqueId()) < 3000) { + player.sendMessage("§cBitte warte einen Moment, bevor du erneut startest."); + return; + } + } + + // Parkour starten + manager.startParkour(player, as.getLocation()); + player.sendMessage("§8[§6Parkour§8] §eViel Erfolg! Erreiche das Ziel so schnell wie möglich."); + player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1f); + + // Event abbrechen, damit man keine Ausrüstung vom ArmorStand klaut + event.setCancelled(true); + } + } + + @EventHandler + public void onMove(PlayerMoveEvent event) { + // Performance: Nur berechnen, wenn ein voller Block gewechselt wurde + if (event.getFrom().getBlockX() == event.getTo().getBlockX() && + event.getFrom().getBlockY() == event.getTo().getBlockY() && + event.getFrom().getBlockZ() == event.getTo().getBlockZ()) return; + + Player player = event.getPlayer(); + if (!manager.isIngame(player)) return; + + Location loc = player.getLocation(); + + // --- 1. ABSTURZ-CHECK --- + // Passe die Höhe '50' an deine Map an! + if (loc.getY() < 50) { + Location lastCp = manager.getCheckpoint(player); + if (lastCp != null) { + player.teleport(lastCp); + player.sendMessage("§8[§6Parkour§8] §cAbgestürzt! Zurück zum Checkpoint."); + player.playSound(player.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 0.5f, 1.0f); + } + return; + } + + // --- 2. CHECKPOINT-CHECK --- + List cps = manager.getOrderedCheckpoints(); + for (int i = 0; i < cps.size(); i++) { + if (isNearby(loc, cps.get(i))) { + manager.reachCheckpoint(player, i); + } + } + + // --- 3. ZIEL-CHECK --- + Location finish = manager.getFinishLocation(); + if (finish != null && isNearby(loc, finish)) { + manager.finishParkour(player); + // Nach dem Ziel setzen wir einen Cooldown + startCooldown.put(player.getUniqueId(), System.currentTimeMillis()); + } + } + + private boolean isNearby(Location playerLoc, Location targetLoc) { + if (playerLoc.getWorld() == null || !playerLoc.getWorld().equals(targetLoc.getWorld())) return false; + // 2.25 entspricht einem Radius von ca. 1.5 Blöcken + return playerLoc.distanceSquared(targetLoc) <= 2.25; + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + manager.stopParkour(event.getPlayer()); + startCooldown.remove(uuid); + } +} \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/parkour/ParkourManager.java b/src/main/java/de/nexuslobby/modules/parkour/ParkourManager.java new file mode 100644 index 0000000..6ee6c8f --- /dev/null +++ b/src/main/java/de/nexuslobby/modules/parkour/ParkourManager.java @@ -0,0 +1,231 @@ +package de.nexuslobby.modules.parkour; + +import de.nexuslobby.NexusLobby; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +public class ParkourManager { + + private final NexusLobby plugin; + private final File file; + private FileConfiguration config; + + private final Map startTime = new HashMap<>(); + private final Map lastCheckpointLoc = new HashMap<>(); + private final Map nextCheckpointIndex = new HashMap<>(); + + public ParkourManager(NexusLobby plugin) { + this.plugin = plugin; + this.file = new File(plugin.getDataFolder(), "parkour.yml"); + loadConfig(); + startParticleTask(); + } + + private void loadConfig() { + if (!file.exists()) { + file.getParentFile().mkdirs(); + try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } + } + config = YamlConfiguration.loadConfiguration(file); + } + + public void setCheckpoint(Player player, Location loc) { + int nextId = 1; + if (config.contains("locations.checkpoints") && config.getConfigurationSection("locations.checkpoints") != null) { + nextId = config.getConfigurationSection("locations.checkpoints").getKeys(false).size() + 1; + } + + addCheckpointLocation(String.valueOf(nextId), loc); + player.sendMessage("§8[§6Parkour§8] §7Checkpoint §e#" + nextId + " §7an deiner Position gesetzt."); + player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1.0f, 1.5f); + } + + /** + * Löscht die gesamte Strecke (Checkpoints und Ziel) und bricht aktuelle Läufe ab. + */ + public void removeAllPoints() { + config.set("locations.checkpoints", null); + config.set("locations.finish", null); + save(); + + // Alle Spieler aus dem Parkour werfen, damit keine Partikel zu gelöschten Zielen führen + for (UUID uuid : new HashSet<>(startTime.keySet())) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { + stopParkour(p); + p.sendMessage("§8[§6Parkour§8] §cDie aktuelle Strecke wurde soeben gelöscht."); + } + } + } + + public void startParkour(Player player, Location startLoc) { + if (startTime.containsKey(player.getUniqueId())) return; + + startTime.put(player.getUniqueId(), System.currentTimeMillis()); + lastCheckpointLoc.put(player.getUniqueId(), startLoc); + nextCheckpointIndex.put(player.getUniqueId(), 0); + + player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_CHIME, 1.0f, 1.2f); + + new BukkitRunnable() { + @Override + public void run() { + if (!isIngame(player) || !player.isOnline()) { + this.cancel(); + return; + } + long diff = System.currentTimeMillis() - startTime.get(player.getUniqueId()); + double sec = diff / 1000.0; + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, + new TextComponent("§6⏱ Zeit: §e" + String.format("%.2f", sec) + "s §8| §bNächster Punkt: §7Partikel folgen")); + } + }.runTaskTimer(plugin, 0L, 1L); + } + + private void startParticleTask() { + new BukkitRunnable() { + @Override + public void run() { + for (UUID uuid : startTime.keySet()) { + Player p = Bukkit.getPlayer(uuid); + if (p == null) continue; + + int nextIdx = nextCheckpointIndex.getOrDefault(uuid, 0); + List cps = getOrderedCheckpoints(); + + if (nextIdx < cps.size()) { + Location nextCp = cps.get(nextIdx); + if (nextCp != null && p.getWorld().equals(nextCp.getWorld())) { + p.spawnParticle(Particle.SOUL_FIRE_FLAME, nextCp.clone().add(0, 0.5, 0), 5, 0.1, 0.3, 0.1, 0.02); + } + } else { + Location finish = getFinishLocation(); + if (finish != null && p.getWorld().equals(finish.getWorld())) { + p.spawnParticle(Particle.HAPPY_VILLAGER, finish.clone().add(0, 0.5, 0), 8, 0.2, 0.5, 0.2, 0.02); + } + } + } + } + }.runTaskTimer(plugin, 0L, 6L); + } + + public void reachCheckpoint(Player player, int reachedIndex) { + int currentNext = nextCheckpointIndex.getOrDefault(player.getUniqueId(), 0); + + if (reachedIndex == currentNext) { + List cps = getOrderedCheckpoints(); + if (reachedIndex < cps.size()) { + lastCheckpointLoc.put(player.getUniqueId(), cps.get(reachedIndex)); + nextCheckpointIndex.put(player.getUniqueId(), reachedIndex + 1); + + player.sendMessage("§8[§6Parkour§8] §bCheckpoint #" + (reachedIndex + 1) + " erreicht!"); + player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.8f, 1.5f); + } + } + } + + public void finishParkour(Player player) { + if (!startTime.containsKey(player.getUniqueId())) return; + + if (nextCheckpointIndex.getOrDefault(player.getUniqueId(), 0) < getOrderedCheckpoints().size()) { + player.sendMessage("§cDu hast Checkpoints übersprungen! Folge den Partikeln."); + return; + } + + long duration = System.currentTimeMillis() - startTime.get(player.getUniqueId()); + double seconds = duration / 1000.0; + + player.sendMessage("§8§m--------------------------------------"); + player.sendMessage("§8[§6Parkour§8] §a§lZiel erreicht!"); + player.sendMessage("§7Deine Zeit: §e" + String.format("%.2f", seconds) + "s"); + player.sendMessage("§8§m--------------------------------------"); + + player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f); + + saveBestTime(player, seconds); + stopParkour(player); + } + + public void stopParkour(Player player) { + startTime.remove(player.getUniqueId()); + lastCheckpointLoc.remove(player.getUniqueId()); + nextCheckpointIndex.remove(player.getUniqueId()); + } + + public void setStartLocation(Location loc) { config.set("locations.start", loc); save(); } + public void setFinishLocation(Location loc) { config.set("locations.finish", loc); save(); } + public void addCheckpointLocation(String id, Location loc) { config.set("locations.checkpoints." + id, loc); save(); } + + public Location getStartLocation() { return config.getLocation("locations.start"); } + public Location getFinishLocation() { return config.getLocation("locations.finish"); } + + public List getOrderedCheckpoints() { + if (!config.contains("locations.checkpoints") || config.getConfigurationSection("locations.checkpoints") == null) + return new ArrayList<>(); + + return config.getConfigurationSection("locations.checkpoints").getKeys(false).stream() + .sorted(Comparator.comparingInt(Integer::parseInt)) + .map(key -> config.getLocation("locations.checkpoints." + key)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private void save() { try { config.save(file); } catch (IOException e) { e.printStackTrace(); } } + + public void clearStats() { + config.set("besttimes", null); + config.set("names", null); + save(); + } + + public boolean isIngame(Player player) { return startTime.containsKey(player.getUniqueId()); } + public Location getCheckpoint(Player player) { return lastCheckpointLoc.get(player.getUniqueId()); } + + private void saveBestTime(Player player, double time) { + String path = "besttimes." + player.getUniqueId(); + double currentTime = config.getDouble(path, 99999.9); + if (time < currentTime) { + config.set(path, time); + config.set("names." + player.getUniqueId(), player.getName()); + save(); + player.sendMessage("§8[§6Parkour§8] §6§lNeuer Rekord! §7Du hast dich verbessert."); + } + } + + public String getTopTen() { + if (!config.contains("besttimes") || config.getConfigurationSection("besttimes") == null) + return "§6§l🏆 TOP 10 PARKOUR 🏆\n§7Noch keine Rekorde."; + + Map allTimes = new HashMap<>(); + for (String uuidStr : config.getConfigurationSection("besttimes").getKeys(false)) { + allTimes.put(uuidStr, config.getDouble("besttimes." + uuidStr)); + } + + List> sortedList = allTimes.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .limit(10) + .toList(); + + StringBuilder builder = new StringBuilder("§6§l🏆 TOP 10 PARKOUR 🏆"); + int rank = 1; + for (Map.Entry entry : sortedList) { + String name = config.getString("names." + entry.getKey(), "Unbekannt"); + builder.append("\n§e#").append(rank).append(" §f").append(name).append(" §8» §a").append(String.format("%.2f", entry.getValue())).append("s"); + rank++; + } + return builder.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/player/PlayerInspectModule.java b/src/main/java/de/nexuslobby/modules/player/PlayerInspectModule.java new file mode 100644 index 0000000..35f8679 --- /dev/null +++ b/src/main/java/de/nexuslobby/modules/player/PlayerInspectModule.java @@ -0,0 +1,188 @@ +package de.nexuslobby.modules.player; + +import de.nexuslobby.NexusLobby; +import de.nexuslobby.api.Module; +import me.clip.placeholderapi.PlaceholderAPI; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; + +import java.util.ArrayList; +import java.util.List; + +/** + * Modul zur Inspektion von Spielern in der Lobby. + * Zeigt detaillierte Statistiken in einer interaktiven GUI an. + */ +public class PlayerInspectModule implements Module, Listener { + + @Override + public String getName() { + return "PlayerInspect"; + } + + @Override + public void onEnable() { + Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); + } + + @Override + public void onDisable() {} + + @EventHandler + public void onPlayerInteract(PlayerInteractEntityEvent event) { + // Prüfen, ob ein Spieler rechtsgeklickt wurde + if (event.getRightClicked() instanceof Player target) { + Player viewer = event.getPlayer(); + + // GUI nur öffnen, wenn die Hand leer ist (verhindert Konflikte mit Items) + if (viewer.getInventory().getItemInMainHand().getType() == Material.AIR) { + openDetailedInspectGUI(viewer, target); + } + } + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + // Sicherstellen, dass es unser Statistik-Inventar ist anhand des Titels + if (event.getView().getTitle().contains("Statistiken")) { + event.setCancelled(true); // Verhindert, dass Items herausgenommen werden + + ItemStack clickedItem = event.getCurrentItem(); + if (clickedItem == null || clickedItem.getType() == Material.AIR) return; + + // Logik für den Schließen-Button (Barriere) + if (clickedItem.getType() == Material.BARRIER) { + event.getWhoClicked().closeInventory(); + } + } + } + + public void openDetailedInspectGUI(Player viewer, Player target) { + // Titel mit Farbcodes + String title = "§8» §3Statistiken: §b" + target.getName(); + Inventory gui = Bukkit.createInventory(null, 45, title); + + // --- Design: Hintergrund mit Glasscheiben füllen --- + ItemStack separator = createSimpleItem(Material.GRAY_STAINED_GLASS_PANE, " "); + int[] borderSlots = { + 0,1,2,3,4,5,6,7,8, + 9,17, + 18,26, + 27,35, + 36,37,38,39,41,42,43,44 + }; + for (int slot : borderSlots) gui.setItem(slot, separator); + + // --- Kopf des Spielers mit LuckPerms Prefix via PAPI --- + ItemStack head = new ItemStack(Material.PLAYER_HEAD); + SkullMeta headMeta = (SkullMeta) head.getItemMeta(); + if (headMeta != null) { + headMeta.setOwningPlayer(target); + headMeta.setDisplayName("§e§l" + target.getName()); + + List lore = new ArrayList<>(); + lore.add("§8§m-----------------------"); + + // LuckPerms Prefix über PlaceholderAPI auslesen + String prefix = "%luckperms_prefix%"; + prefix = PlaceholderAPI.setPlaceholders(target, prefix); + + // KORREKTUR: Farbcodes (&) in echte Minecraft-Farben (§) umwandeln + prefix = ChatColor.translateAlternateColorCodes('&', prefix); + + lore.add("§7Rang: " + (prefix.isEmpty() ? "§fSpieler" : prefix)); + lore.add("§7Level: §a" + target.getLevel()); + lore.add("§7Status: §aOnline"); + lore.add("§8§m-----------------------"); + + headMeta.setLore(lore); + head.setItemMeta(headMeta); + } + gui.setItem(4, head); + + // --- Statistik Kategorie: KAMPF (Slot 19) --- + gui.setItem(19, createStatsItem(Material.DIAMOND_SWORD, "§c§lKampf-Statistiken", + "§8» §7Spieler-Kills: §f" + target.getStatistic(Statistic.PLAYER_KILLS), + "§8» §7Mob-Kills: §f" + target.getStatistic(Statistic.MOB_KILLS), + "§8» §7Tode gesamt: §f" + target.getStatistic(Statistic.DEATHS), + "§8» §7Schaden verursacht: §f" + (target.getStatistic(Statistic.DAMAGE_DEALT) / 10) + " ❤")); + + // --- Statistik Kategorie: ARBEIT (Slot 21) --- + // Optimierte Abfrage für wichtige Blöcke + int totalBlocks = target.getStatistic(Statistic.MINE_BLOCK, Material.STONE) + + target.getStatistic(Statistic.MINE_BLOCK, Material.DIRT) + + target.getStatistic(Statistic.MINE_BLOCK, Material.COBBLESTONE); + + gui.setItem(21, createStatsItem(Material.IRON_PICKAXE, "§e§lHandwerk & Fleiß", + "§8» §7Blöcke (S/D/C): §f" + totalBlocks, + "§8» §7Items gedroppt: §f" + target.getStatistic(Statistic.DROP_COUNT), + "§8» §7Fische gefangen: §f" + target.getStatistic(Statistic.FISH_CAUGHT), + "§8» §7Glocken geläutet: §f" + target.getStatistic(Statistic.BELL_RING))); + + // --- Statistik Kategorie: BEWEGUNG (Slot 23) --- + double walkKm = target.getStatistic(Statistic.WALK_ONE_CM) / 100000.0; + double flyKm = target.getStatistic(Statistic.FLY_ONE_CM) / 100000.0; + gui.setItem(23, createStatsItem(Material.GOLDEN_BOOTS, "§b§lReise-Statistiken", + "§8» §7Gelaufen: §f" + String.format("%.2f", walkKm) + " km", + "§8» §7Geflogen: §f" + String.format("%.2f", flyKm) + " km", + "§8» §7Sprünge: §f" + target.getStatistic(Statistic.JUMP))); + + // --- Statistik Kategorie: ZEIT (Slot 25) --- + long ticks = target.getStatistic(Statistic.PLAY_ONE_MINUTE); + long hours = ticks / 72000; + long mins = (ticks % 72000) / 1200; + gui.setItem(25, createStatsItem(Material.CLOCK, "§a§lZeit-Statistiken", + "§8» §7Spielzeit: §f" + hours + " Std. " + mins + " Min.", + "§8» §7Letzter Tod vor: §f" + (target.getStatistic(Statistic.TIME_SINCE_DEATH) / 1200) + " Min.", + "§8» §7Tage auf Server: §f" + (target.getStatistic(Statistic.PLAY_ONE_MINUTE) / 1728000))); + + // --- Schließen Button (Slot 40) --- + gui.setItem(40, createSimpleItem(Material.BARRIER, "§c§lMenü schließen")); + + // Inventar für den Zuschauer öffnen + viewer.openInventory(gui); + } + + /** + * Erstellt ein ItemStack mit Name und Lore-Zeilen inklusive einer Leerzeile am Anfang. + */ + private ItemStack createStatsItem(Material material, String displayName, String... loreLines) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(displayName); + List lore = new ArrayList<>(); + lore.add(" "); // Leerzeile für sauberes Design + for (String line : loreLines) { + lore.add(line); + } + meta.setLore(lore); + item.setItemMeta(meta); + } + return item; + } + + /** + * Erstellt ein einfaches ItemStack ohne Lore für Designzwecke. + */ + private ItemStack createSimpleItem(Material material, String displayName) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(displayName); + item.setItemMeta(meta); + } + return item; + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index d27a3fd..0ce575c 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -97,6 +97,13 @@ compass: lore: - "&7Zeige was du kannst!" +# ----------------------------------------------------- +# PLAYER INSPECT (Statistiken per Rechtsklick) +# ----------------------------------------------------- +player_inspect: + enabled: true + gui_title: "&8Statistiken von &6{PLAYER}" + # --- Suppressor / Global Chat Einstellungen --- suppressor: enabled: true diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 6284aee..fb15ad1 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,9 +1,9 @@ name: NexusLobby main: de.nexuslobby.NexusLobby -version: "1.0.5" +version: "1.0.8" api-version: "1.21" author: M_Viper -description: Modular Lobby Plugin +description: Modular Lobby Plugin with an invisible Particle-Parkour system. softdepend: [LuckPerms, PlaceholderAPI, Vault, WorldGuard] commands: @@ -38,8 +38,8 @@ commands: permission: nexuslobby.build permission-message: "§cDu hast keine Rechte!" nexuslobby: - description: Zeigt Informationen über das Plugin an oder lädt es neu - usage: /nexuslobby [reload|setspawn] + description: Hauptbefehl für Plugin-Verwaltung, Spawn-Setup und Parkour-Konfiguration + usage: /nexuslobby [reload|setspawn|silentjoin|sb|parkour] aliases: [nexus, lobby] nexustools: description: Nexus ArmorStand Editor (LookAt, Dynamic, etc.) @@ -70,6 +70,20 @@ commands: description: Teleportiert dich zum Lobby-Spawnpunkt usage: /spawn aliases: [l, hub] + + # --- NEUE PARKOUR DIREKT-BEFEHLE --- + setstart: + description: Markiert einen ArmorStand als Parkour-NPC oder setzt die Start-Location + usage: /setstart + permission: nexuslobby.admin + setcheckpoint: + description: Setzt einen neuen Checkpoint an deiner aktuellen Position + usage: /setcheckpoint + permission: nexuslobby.admin + setfinish: + description: Setzt den Zielpunkt für den Parkour + usage: /setfinish + permission: nexuslobby.admin permissions: nexuslobby.portal: @@ -85,7 +99,7 @@ permissions: description: Zugriff auf den Server Switcher default: true nexuslobby.admin: - description: Voller Zugriff auf Lobby-Gamerules, Einstellungen, Intro, Border und Reload + description: Voller Zugriff auf Lobby-Gamerules, Einstellungen, Intro, Border, Parkour-Setup und Reload default: op nexuslobby.build: description: Erlaubt das Umgehen des Lobby-Schutzes zum Bauen @@ -107,4 +121,10 @@ permissions: default: op nexuslobby.mapart: description: Erlaubt das Erstellen von Map-Art Bildern + default: op + nexuslobby.silentjoin: + description: Versteckt die Join-Nachricht für den Spieler (Silent Join) + default: op + nexuslobby.parkour.admin: + description: Erlaubt das Setzen der unsichtbaren Parkour-Punkte (Start, Ziel, Checkpoints) default: op \ No newline at end of file