From 3a8fe5cf66922cd991e02ea2de39f2045d5be645 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sun, 11 Oct 2026 17:15:33 +0000 Subject: [PATCH] Upload via GUI (84 Dateien) --- readme.md | 400 +++++ .../commands/NexusLobbyCommand.java | 10 +- .../de/nexuslobby/modules/BuildModule.java | 92 +- .../modules/armorstandtools/ASTListener.java | 2 +- .../armorstandtools/AnvilRenameGUI.java | 188 +- .../ArmorStandCmdExecutor.java | 54 +- .../armorstandtools/ArmorStandCommand.java | 2 +- .../ArmorStandLookAtModule.java | 224 +-- .../armorstandtools/ArmorStandPoseGUI.java | 220 +-- .../ArmorStandStatusModule.java | 268 +-- .../armorstandtools/ArmorStandTool.java | 6 +- .../armorstandtools/ConversationManager.java | 592 +++--- .../DynamicArmorStandModule.java | 260 +-- .../nexuslobby/modules/ball/SoccerModule.java | 1588 ++++++++--------- .../modules/border/BorderCommand.java | 214 +-- .../modules/border/BorderModule.java | 322 ++-- .../nexuslobby/modules/gadgets/FreezeRay.java | 4 +- .../modules/gadgets/GadgetModule.java | 34 +- .../modules/gadgets/GadgetShield.java | 4 +- .../modules/gadgets/HatManager.java | 2 +- .../modules/gadgets/MeteorStrike.java | 4 +- .../modules/hologram/HoloCommand.java | 178 +- .../modules/hologram/HologramModule.java | 440 ++--- .../modules/hologram/NexusHologram.java | 234 +-- .../nexuslobby/modules/intro/IntroModule.java | 728 ++++---- .../modules/mapart/MapArtModule.java | 618 +++---- .../modules/parkour/ParkourListener.java | 212 +-- .../modules/parkour/ParkourManager.java | 926 +++++----- 28 files changed, 4113 insertions(+), 3713 deletions(-) create mode 100644 readme.md diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..04fa1f5 --- /dev/null +++ b/readme.md @@ -0,0 +1,400 @@ +## 🌐 Mehrsprachigkeit & Texte + +Alle Nachrichten, Hilfetexte und Fehler werden zentral über die Datei `lang.yml` im Ordner `src/main/resources` verwaltet. Dort kannst du für jede Sprache (z.B. Deutsch und Englisch) die Texte pflegen und beliebig erweitern. + +**Beispiel für lang.yml:** + +```yaml +welcome: + de: "Willkommen auf dem Server!" + en: "Welcome to the server!" +no_permission: + de: "§cKeine Berechtigung." + en: "§cNo permission." +``` + +**Sprache umstellen:** +Im Code kann die Sprache mit `LangManager.setLanguage("en")` gewechselt werden. Standard ist Deutsch (`de`). + +**Texte ingame nutzen:** +Alle Texte werden im Plugin mit `LangManager.get("key")` abgerufen und sind direkt ingame sichtbar. Änderungen in der lang.yml wirken nach einem Reload sofort. + +**Chat-Prefix anpassen:** +Der Prefix vor Plugin-Nachrichten wird in der Server-`config.yml` über `prefix` festgelegt. Farben können mit `&`-Codes angegeben werden: + +```yaml +prefix: "&8[&6Nexus&8] " +``` + +Ändere den Text und die Farben nach Wunsch und lade das Plugin anschließend neu. + +--- +# NexusLobby + +

+ NexusLobby +

+ +Ein umfassendes, modulares Lobby-Plugin für Minecraft Server (Paper/Spigot 1.21+) mit High-End NPC-System, umfangreichen Sicherheitsfunktionen, Soccer-Modul, Parkour-System und voller Konfigurierbarkeit. + +![Minecraft](https://img.shields.io/badge/Minecraft-1.21+-green) +![Java](https://img.shields.io/badge/Java-21+-orange) +![License](https://img.shields.io/badge/License-Proprietary-red) +![Version](https://img.shields.io/badge/Version-1.1.0-blue) + +--- + +## 🎯 Lizenz und Nutzungsbedingungen + +**ALLE RECHTE VORBEHALTEN** + +Dieses Plugin ist urheberrechtlich geschützt. Es gelten folgende Bedingungen: + +# Scoreboard ausblenden (Opt-out) + +Spieler können das Scoreboard per Befehl ein-/ausblenden. Standardmäßig wird das Scoreboard beim Join angezeigt. Über die Option + +```yaml +scoreboard-default-visible: true +``` + +in der `config.yml` kann festgelegt werden, ob das Scoreboard für neue Spieler standardmäßig sichtbar ist. Bei `false` ist es für alle Spieler nach dem Join ausgeblendet, bis sie es selbst aktivieren. + +- Die Weitergabe, Verbreitung oder Veröffentlichung des Plugins ist **strengstens untersagt**. +- Jegliche Änderung, Modifikation oder Dekompilierung des Codes ist **verboten**. +- Das Plugin darf nicht verkauft, vermietet oder anderweitig kommerziell genutzt werden. +- Eine Weitergabe an Dritte ist ohne ausdrückliche schriftliche Genehmigung nicht gestattet. + +Bei Verstoß gegen diese Bedingungen behalten wir uns rechtliche Schritte vor. + +--- + +## ✨ Features + +### 🤖 High-End NPC & ArmorStand System +- **Conversation Manager** - Komplexe Dialoge zwischen NPCs mit Sprechblasen und Sound-Effekten +- **Dynamic KI** - NPCs reagieren auf Tageszeit (Fackel nachts) und ziehen bei Annäherung von Spielern das Schwert +- **LookAt-Logik** - NPCs verfolgen flüssig die Kopfbewegungen von Spielern in der Nähe +- **Command Binding** - Binde Spieler-, Konsolen- oder Bungee-Befehle an NPC-Slots (0-9) +- **Status-Backup** - Automatisches Speichern von NPC-Namen via PersistentDataContainer & Tags + +### ⚽ Soccer/Fußball-System +- **Interaktiver Soccer-Ball** - Physics-basierter Ball mit realistischer Physik +- **Dribbling** - Ball folgt Spielern automatisch in der Nähe +- **Kick-Mechanik** - Schieße den Ball durch Schlagen oder Angriff +- **Wall-Bounce** - Realistische Wand-Abpraller mit Sound-Effekten +- **Partikel-System** - Geschwindigkeits-abhängige Partikel (SONIC_BOOM, CRIT, SMOKE) +- **Auto-Respawn** - Automatischer Respawn bei Inaktivität oder Void-Fall +- **Anti-Duplikat-System** - Verhindert mehrfache Ball-Instanzen + +### 🏃 Parkour-System +- **Unsichtbare Checkpoints** - Partikel-basierte Checkpoint-Markierungen +- **Timer-System** - Präzise Zeitmessung mit ActionBar-Anzeige +- **Bestenliste** - Persistente Top-10 Bestzeiten-Speicherung +- **NPC-Trainer** - Interaktive NPCs zum Starten des Parkours +- **Fail-Safe** - Teleport zum letzten Checkpoint bei Fehler + +### 🌍 Lobby-Management +- **Spawn-System** - Automatischer Teleport zum Spawn bei Join/Respawn +- **Portal-System** - BungeeCord-Portale für nahtlose Server-Wechsel mit Partikel-Effekten +- **Build-Modus** - Schnelles Umschalten zwischen Bau- und Spielmodus +- **Double-Jump** - Konfigurierbarer Doppelsprung mit Cooldown und Partikeln +- **Worldborder** - Unsichtbare Lobby-Begrenzung (Circle/Square) +- **Intro-Tour** - Cinematic Kamera-Tour für neue Spieler + +### 🛡️ Sicherheit & Protection +- **VPN-Blocker** - Blockiert VPN/Proxy-Verbindungen via proxycheck.io API +- **Country-Blocker** - Geo-IP Filter (Whitelist/Blacklist für Länder) +- **Maintenance-Modus** - Vollständiger Wartungsmodus mit Whitelist-Funktion +- **World-Protection** - Schutz vor Griefing, Hunger, Fallschaden und PvP +- **Anti-Grief** - Verhindert Block-Break, Item-Drop und Explosionen +- **Security-Module** - Umfassende Sicherheitsprüfungen beim Join + +### 📊 Visuelle Elemente +- **Scoreboard** - Vollständig animiert mit PlaceholderAPI-Support +- **Tablist** - Dynamische Header/Footer mit Permissions-Anzeige +- **BossBar** - Rotierende Nachrichten mit Farb-Effekten +- **ActionBar** - Typewriter-Effekt für permanente Status-Anzeigen +- **Hologramme** - Text-Displays mit flexibler Positionierung +- **MapArt** - Bild-Generator für Item-Frames aus URLs + +### 🎮 Item-System +- **Compass** - Server-Switcher GUI +- **Player-Hider** - Sichtbarkeits-Toggle für andere Spieler +- **Gadgets** - Konfigurierbares Item-System mit Custom-Slots +- **Player-Inspector** - Detaillierte Spieler-Informationen per Click + +--- + +## 📋 Befehle + +### Haupt-Commands + +| Befehl | Beschreibung | Berechtigung | +|--------|--------------|--------------| +| `/nexuslobby reload` | Lädt das Plugin neu | `nexuslobby.admin` | +| `/nexuslobby setspawn` | Setzt den Lobby-Spawn | `nexuslobby.admin` | +| `/nexuslobby silentjoin ` | Versteckter Join/Quit | `nexuslobby.admin` | +| `/nexuslobby sb ` | Scoreboard-Kontrolle | `nexuslobby.admin` | +| `/spawn` | Teleport zum Spawn | `nexuslobby.spawn` | + +### NPC & ArmorStand + +| Befehl | Beschreibung | Berechtigung | +|--------|--------------|--------------| +| `/nexustools` | NPC Editor GUI (Rotation, KI) | `nexuslobby.armorstand.use` | +| `/nexuscmd add [cmd]` | Bindet Command an NPC | `nexuslobby.armorstand.cmd` | +| `/nexuscmd remove ` | Entfernt Commands | `nexuslobby.armorstand.cmd` | +| `/nexuscmd conv select1-4` | Wählt NPCs für Dialog | `nexuslobby.armorstand.cmd` | +| `/nexuscmd conv link ` | Verknüpft Dialog | `nexuslobby.armorstand.cmd` | +| `/nexuscmd say ` | NPC-Sprechblase | `nexuslobby.armorstand.cmd` | + +### Parkour + +| Befehl | Beschreibung | Berechtigung | +|--------|--------------|--------------| +| `/nexuslobby parkour setstart` | Markiert Start-NPC | `nexuslobby.admin` | +| `/nexuslobby parkour setcheckpoint ` | Setzt Checkpoint | `nexuslobby.admin` | +| `/nexuslobby parkour setfinish` | Setzt Ziel | `nexuslobby.admin` | +| `/nexuslobby parkour reset` | Löscht eigene Zeit | `nexuslobby.admin` | +| `/nexuslobby parkour clear` | Löscht alle Zeiten | `nexuslobby.admin` | +| `/setstart` | Alias für setstart | `nexuslobby.admin` | +| `/setcheckpoint` | Alias für setcheckpoint | `nexuslobby.admin` | +| `/setfinish` | Alias für setfinish | `nexuslobby.admin` | + +### Soccer/Fußball + +| Befehl | Beschreibung | Berechtigung | +|--------|--------------|--------------| +| `/nexuslobby ball setspawn` | Setzt Ball-Spawn | `nexuslobby.admin` | +| `/nexuslobby ball respawn` | Spawnt Ball neu | `nexuslobby.admin` | +| `/nexuslobby ball remove` | Entfernt Ball | `nexuslobby.admin` | + +### Portal & Server + +| Befehl | Beschreibung | Berechtigung | +|--------|--------------|--------------| +| `/portal create ` | Erstellt Portal | `nexuslobby.portal` | +| `/portal delete ` | Löscht Portal | `nexuslobby.portal` | +| `/portal list` | Zeigt alle Portale | `nexuslobby.portal` | +| `/giveportalwand` | Portal-Werkzeug | `nexuslobby.portal.give` | +| `/serverswitcher` | Öffnet Server-GUI | `nexuslobby.serverswitcher` | + +### Sonstiges + +| Befehl | Beschreibung | Berechtigung | +|--------|--------------|--------------| +| `/build` | Build-Modus toggle | `nexuslobby.build` | +| `/maintenance ` | Wartungsmodus | `nexuslobby.maintenance` | +| `/settings` | Lobby-Einstellungen (Gamerules) | `nexuslobby.admin` | +| `/holo create [text]` | Erstellt Hologramm | `nexuslobby.hologram` | +| `/holo delete ` | Löscht Hologramm | `nexuslobby.hologram` | +| `/mapart ` | Erstellt MapArt | `nexuslobby.mapart` | +| `/intro ` | Intro-Tour-Verwaltung | `nexuslobby.admin` | +| `/border ` | Worldborder-Setup | `nexuslobby.admin` | + +--- + +## ⚙️ Konfiguration + +### config.yml (Auszug) + +```yaml +# Spawn-Einstellungen +spawn: + world: "world" + x: 0.5 + y: 64.0 + z: 0.5 + yaw: 0.0 + pitch: 0.0 + +# Lobby-Einstellungen +lobby: + allow-fly: false + pvp-enabled: false + build-enabled: false + default-gamemode: Adventure + clear-inventory-on-join: true + +# Server-Mapping (für Status-Check) +servers: + survival: + ip: "127.0.0.1" + port: 25566 + skyblock: + ip: "127.0.0.1" + port: 25567 + +# Ball/Soccer-Einstellungen +ball: + respawn_delay: 60 # Sekunden bis Auto-Respawn + +# Worldborder +worldborder: + enabled: true + type: "SQUARE" # SQUARE oder CIRCLE + radius: 50.0 +``` + +### conversations.yml + +```yaml +conversations: + willkommen: + dialogue: + - "&eWächter: &7Willkommen auf dem Server!" + - "&aBürger: &7Schön, dass du da bist!" + - "&eWächter: &7Viel Spaß beim Erkunden!" + + quest_dialog: + dialogue: + - "&6Quest-Geber: &7Hast du meine Quest erledigt?" + - "&aHeld: &7Ja, ich habe alle Monster besiegt!" + +links: + # UUID des ersten NPCs + "550e8400-e29b-41d4-a716-446655440000": + partner: "550e8400-e29b-41d4-a716-446655440001" # UUID des zweiten NPCs + dialog: willkommen + automated: true # Startet automatisch +``` + +### visuals.yml (Auszug) + +```yaml +# ActionBar-Nachrichten +actionbar: + enabled: true + speed: 3 + messages: + - "&6&lWillkommen &8» &eauf unserem Server!" + - "&b&lViel Spaß &8» &7in der Lobby!" + +# BossBar +bossbar: + enabled: true + interval: 40 + messages: + - text: "&6&lNexusLobby &8» &eVersion 1.1.0" + color: "YELLOW" +``` + +--- + +## 🔐 Berechtigungen + +### Admin-Berechtigungen + +| Berechtigung | Beschreibung | +|--------------|--------------| +| `nexuslobby.admin` | Voller Zugriff auf alle System-Einstellungen | +| `nexuslobby.armorstand.cmd` | NPCs konfigurieren und Dialoge verknüpfen | +| `nexuslobby.armorstand.use` | Zugriff auf die ArmorStand-Editor GUI | +| `nexuslobby.build` | Berechtigung für den Baumodus | +| `nexuslobby.portal` | Portale erstellen und löschen | +| `nexuslobby.portal.give` | Portal-Werkzeug erhalten | +| `nexuslobby.hologram` | Hologramme verwalten | +| `nexuslobby.mapart` | MapArt erstellen | +| `nexuslobby.maintenance` | Wartungsmodus togglen | + +### Spieler-Berechtigungen + +| Berechtigung | Beschreibung | +|--------------|--------------| +| `nexuslobby.spawn` | Spawn-Command nutzen | +| `nexuslobby.serverswitcher` | Server-Switcher GUI öffnen | +| `nexuslobby.scoreboard.admin` | Admin-Scoreboard sehen | + +### Bypass-Berechtigungen + +| Berechtigung | Beschreibung | +|--------------|--------------| +| `nexuslobby.bypass.maintenance` | Server trotz Wartungsmodus betreten | +| `nexuslobby.bypass.vpn` | VPN-Check überspringen | +| `nexuslobby.bypass.country` | Country-Check überspringen | + +--- + +## 🔧 Technische Details + +### Systemanforderungen +- **Minecraft**: Paper/Spigot 1.21+ (Spigot-kompatibel, Paper empfohlen) +- **Java**: Java 21 oder höher +- **Dependencies**: LuckPerms, PlaceholderAPI (optional) + +### Verwendete Technologien +- **Build-Tool**: Maven 3.9+ +- **JSON-Library**: Gson 2.10.1 (gegenüber json-simple 1.1.1 aktualisiert) +- **BungeeCord API**: Für Server-Wechsel und Messaging +- **PlaceholderAPI**: Für dynamische Platzhalter + +### Code-Qualität +- ✅ Moderne Java 21 Features (Switch-Expressions, Pattern Matching) +- ✅ Proper Error-Handling (Logger statt printStackTrace) +- ✅ Memory-Management (Tasks & Listeners werden sauber disposed) +- ✅ Config-Validierung mit Default-Werten +- ✅ Optimiertes JSON-Parsing mit Gson +- ✅ Spigot-kompatible BungeeCord Chat API + +### Performance-Optimierungen +- **Async-Processing** für API-Calls (Update-Check, VPN-Check) +- **Caching** für Spieler-Daten und Config-Werte +- **Optimierte Entity-Queries** (nur relevante Welten) +- **Effiziente Scheduler-Nutzung** (Tasks werden gecancelt) + +--- + +## 📦 Installation + +1. **Download** der neuesten `NexusLobby-1.1.0.jar` aus den [Releases](../../releases) +2. **Upload** der JAR-Datei in den `/plugins/` Ordner +3. **Server-Neustart** durchführen +4. **Konfiguration** anpassen (`config.yml`, `visuals.yml`, etc.) +5. **Permissions** mit LuckPerms oder einem anderen Permissions-Plugin setzen +6. **(Optional)** PlaceholderAPI installieren für erweiterte Platzhalter + +--- + +## 🐛 Bekannte Issues & Lösungen + +### Ball spawnt mehrfach +- **Lösung**: Nutze `/nexuslobby ball remove` gefolgt von `/nexuslobby ball setspawn` +- Das Anti-Duplikat-System sollte dies automatisch verhindern + +### NPCs schauen nicht zu Spielern +- **Lösung**: Stelle sicher, dass der NPC mit `/nexustools` das LookAt-Feature aktiviert hat +- Prüfe, ob `ArmorStandLookAtModule` in den Logs geladen wird + +### Server-Checker zeigt falsche Status +- **Lösung**: Überprüfe die IP/Port-Einstellungen in `config.yml` unter `servers:` +- Stelle sicher, dass die Server erreichbar sind (Firewall, Ports) + +--- + +## 📚 Support & Dokumentation + +- **Wiki**: Ausführliche Dokumentation im [Wiki](../../wiki) +- **Bug-Reports**: Erstelle ein [Issue](../../issues) bei technischen Problemen +- **Feature-Requests**: Vorschläge über [Issues](../../issues) mit Label "enhancement" + +--- + +## 📝 Changelog + +### Version 1.1.0 (Februar 2026) +- ✨ Soccer/Fußball-System mit realistischer Physik +- ✨ Parkour-System mit Bestenliste und Checkpoints +- ✨ Player-Inspector Modul +- ✨ Config-Validierung mit Auto-Defaults +- 🔧 JSON-Library auf Gson 2.10.1 aktualisiert +- 🔧 Alle printStackTrace() durch Logger ersetzt +- 🔧 Memory-Management verbessert (Tasks & Listeners) +- 🔧 UpdateChecker mit proper JSON-Parsing +- 🐛 Diverse Bug-Fixes und Performance-Optimierungen + +--- + +**Copyright © 2026 - M_Viper - Alle Rechte vorbehalten** + +Die unbefugte Vervielfältigung, Verbreitung oder Weitergabe dieses Plugins ist strafbar und wird rechtlich verfolgt. \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java b/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java index 4f3479a..057adb6 100644 --- a/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java +++ b/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java @@ -238,18 +238,18 @@ public class NexusLobbyCommand implements CommandExecutor { */ private int parseTrack(String[] args, int index, Player player) { if (args.length <= index) { - player.sendMessage("§8[§6Nexus§8] §cBitte gib eine Strecken-Nummer an: §e1 §7oder §e2"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cBitte gib eine Strecken-Nummer an: §e1 §7oder §e2"); return -1; } int track; try { track = Integer.parseInt(args[index]); } catch (NumberFormatException e) { - player.sendMessage("§8[§6Nexus§8] §cUngültige Strecken-Nummer. Bitte §e1 §7oder §e2 §7verwenden."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cUngültige Strecken-Nummer. Bitte §e1 §7oder §e2 §7verwenden."); return -1; } if (track < 1 || track > 2) { - player.sendMessage("§8[§6Nexus§8] §cNur Strecke §e1 §7oder §e2 §7sind erlaubt."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cNur Strecke §e1 §7oder §e2 §7sind erlaubt."); return -1; } return track; @@ -285,11 +285,11 @@ public class NexusLobbyCommand implements CommandExecutor { de.nexuslobby.modules.armorstandtools.ConversationManager cm = NexusLobby.getInstance().getConversationManager(); if (cm == null) { - player.sendMessage("§8[§6Nexus§8] §cConversationManager ist nicht aktiv."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cConversationManager ist nicht aktiv."); return; } cm.clearHangingBubbles(); - player.sendMessage("§8[§6Nexus§8] §aAlle hängengebliebenen Sprechblasen wurden entfernt."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aAlle hängengebliebenen Sprechblasen wurden entfernt."); player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 2f); } diff --git a/src/main/java/de/nexuslobby/modules/BuildModule.java b/src/main/java/de/nexuslobby/modules/BuildModule.java index a9e439f..9982f83 100644 --- a/src/main/java/de/nexuslobby/modules/BuildModule.java +++ b/src/main/java/de/nexuslobby/modules/BuildModule.java @@ -1,47 +1,47 @@ -package de.nexuslobby.modules; - -import de.nexuslobby.NexusLobby; -import de.nexuslobby.api.Module; -import org.bukkit.Bukkit; -import org.bukkit.GameMode; -import org.bukkit.entity.Player; -import java.util.ArrayList; -import java.util.UUID; - -public class BuildModule implements Module { - - private final ArrayList buildModePlayers = new ArrayList<>(); - - @Override - public String getName() { - return "Build"; - } - - @Override - public void onEnable() { - // Falls du hier den Command registrierst, achte darauf, - // dass er nicht doppelt in der NexusLobby.java registriert wird. - } - - public void toggleBuildMode(Player player) { - if (!player.hasPermission("nexuslobby.build")) return; - - if (buildModePlayers.contains(player.getUniqueId())) { - buildModePlayers.remove(player.getUniqueId()); - player.setGameMode(GameMode.SURVIVAL); - player.getInventory().clear(); - - // KORREKTUR: Hier muss getItemsModule() mit "s" stehen! - if (NexusLobby.getInstance().getItemsModule() != null) { - NexusLobby.getInstance().getItemsModule().giveLobbyItems(player); - } - } else { - buildModePlayers.add(player.getUniqueId()); - player.setGameMode(GameMode.CREATIVE); - player.getInventory().clear(); - } - } - - @Override - public void onDisable() {} +package de.nexuslobby.modules; + +import de.nexuslobby.NexusLobby; +import de.nexuslobby.api.Module; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import java.util.ArrayList; +import java.util.UUID; + +public class BuildModule implements Module { + + private final ArrayList buildModePlayers = new ArrayList<>(); + + @Override + public String getName() { + return "Build"; + } + + @Override + public void onEnable() { + // Falls du hier den Command registrierst, achte darauf, + // dass er nicht doppelt in der NexusLobby.java registriert wird. + } + + public void toggleBuildMode(Player player) { + if (!player.hasPermission("nexuslobby.build")) return; + + if (buildModePlayers.contains(player.getUniqueId())) { + buildModePlayers.remove(player.getUniqueId()); + player.setGameMode(GameMode.SURVIVAL); + player.getInventory().clear(); + + // KORREKTUR: Hier muss getItemsModule() mit "s" stehen! + if (NexusLobby.getInstance().getItemsModule() != null) { + NexusLobby.getInstance().getItemsModule().giveLobbyItems(player); + } + } else { + buildModePlayers.add(player.getUniqueId()); + player.setGameMode(GameMode.CREATIVE); + player.getInventory().clear(); + } + } + + @Override + public void onDisable() {} } \ 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 d58bb09..a85f739 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ASTListener.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ASTListener.java @@ -148,7 +148,7 @@ public class ASTListener implements Listener { out.writeUTF("Connect"); out.writeUTF(server); player.sendPluginMessage(NexusLobby.getInstance(), "BungeeCord", out.toByteArray()); - player.sendMessage("§8[§6Nexus§8] §7Du wirst mit §e" + server + " §7verbunden..."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§7Du wirst mit §e" + server + " §7verbunden..."); } @EventHandler(priority = EventPriority.LOW) diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/AnvilRenameGUI.java b/src/main/java/de/nexuslobby/modules/armorstandtools/AnvilRenameGUI.java index a7f3326..619752d 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/AnvilRenameGUI.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/AnvilRenameGUI.java @@ -1,95 +1,95 @@ -package de.nexuslobby.modules.armorstandtools; - -import de.nexuslobby.NexusLobby; -import io.papermc.paper.event.player.AsyncChatEvent; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.entity.ArmorStand; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.HandlerList; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerQuitEvent; - -/** - * Chat-basierte Namenseingabe für ArmorStands. - * Der Spieler tippt den neuen Namen in den Chat. - * Unterstützt & Farb-Codes. - */ -public class AnvilRenameGUI implements Listener { - - private final Player player; - private final ArmorStand armorStand; - - public AnvilRenameGUI(Player player, ArmorStand armorStand) { - this.player = player; - this.armorStand = armorStand; - - // Listener registrieren - Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); - - // Inventar schließen + Anweisung anzeigen - player.closeInventory(); - - // Title als UI-Feedback (wie AFK-Anzeige) - player.sendTitle( - "§6§lName eingeben", - "§7Schreibe den neuen Namen in den §eChat§7. §8(&-Codes erlaubt)", - 10, 80, 20 - ); - player.sendMessage("§8[§6Nexus§8] §7Gib den neuen §eNamen §7in den Chat ein. §8(&-Farb-Codes erlaubt)"); - player.sendMessage("§8[§6Nexus§8] §c'abbrechen' §7eingeben zum Abbrechen."); - } - - // ───────────────────────────────────────────────────────────────────────── - // Chat-Event — Paper 1.21 AsyncChatEvent - // ───────────────────────────────────────────────────────────────────────── - - @EventHandler(priority = EventPriority.LOWEST) - public void onChat(AsyncChatEvent event) { - if (!event.getPlayer().equals(player)) return; - - // Chat-Nachricht abfangen - event.setCancelled(true); - - // Plain-Text aus Adventure Component extrahieren - String input = PlainTextComponentSerializer.plainText().serialize(event.message()).trim(); - - // Zurück auf Haupt-Thread wechseln für Bukkit-API Aufrufe - Bukkit.getScheduler().runTask(NexusLobby.getInstance(), () -> { - HandlerList.unregisterAll(this); - AST.chatInputMode.remove(player.getUniqueId()); - - if (input.equalsIgnoreCase("abbrechen")) { - player.sendTitle("§c§lAbgebrochen", "", 5, 30, 10); - player.sendMessage("§8[§6Nexus§8] §cNamensänderung abgebrochen."); - } else { - // & → § Farb-Codes - String newName = ChatColor.translateAlternateColorCodes('&', input); - armorStand.setCustomName(newName); - armorStand.setCustomNameVisible(!ChatColor.stripColor(newName).isEmpty()); - - player.sendTitle("§a§lName gesetzt!", "§r" + newName, 5, 40, 10); - player.sendMessage("§8[§6Nexus§8] §aName gesetzt: " + newName); - } - - // Zurück zum ArmorStand-Editor - if (armorStand.isValid()) { - new ArmorStandGUI(armorStand, player); - } - }); - } - - // ───────────────────────────────────────────────────────────────────────── - // Logout — Listener sauber abmelden - // ───────────────────────────────────────────────────────────────────────── - - @EventHandler - public void onQuit(PlayerQuitEvent event) { - if (!event.getPlayer().equals(player)) return; - HandlerList.unregisterAll(this); - AST.chatInputMode.remove(player.getUniqueId()); - } +package de.nexuslobby.modules.armorstandtools; + +import de.nexuslobby.NexusLobby; +import io.papermc.paper.event.player.AsyncChatEvent; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +/** + * Chat-basierte Namenseingabe für ArmorStands. + * Der Spieler tippt den neuen Namen in den Chat. + * Unterstützt & Farb-Codes. + */ +public class AnvilRenameGUI implements Listener { + + private final Player player; + private final ArmorStand armorStand; + + public AnvilRenameGUI(Player player, ArmorStand armorStand) { + this.player = player; + this.armorStand = armorStand; + + // Listener registrieren + Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); + + // Inventar schließen + Anweisung anzeigen + player.closeInventory(); + + // Title als UI-Feedback (wie AFK-Anzeige) + player.sendTitle( + "§6§lName eingeben", + "§7Schreibe den neuen Namen in den §eChat§7. §8(&-Codes erlaubt)", + 10, 80, 20 + ); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§7Gib den neuen §eNamen §7in den Chat ein. §8(&-Farb-Codes erlaubt)"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§c'abbrechen' §7eingeben zum Abbrechen."); + } + + // ───────────────────────────────────────────────────────────────────────── + // Chat-Event — Paper 1.21 AsyncChatEvent + // ───────────────────────────────────────────────────────────────────────── + + @EventHandler(priority = EventPriority.LOWEST) + public void onChat(AsyncChatEvent event) { + if (!event.getPlayer().equals(player)) return; + + // Chat-Nachricht abfangen + event.setCancelled(true); + + // Plain-Text aus Adventure Component extrahieren + String input = PlainTextComponentSerializer.plainText().serialize(event.message()).trim(); + + // Zurück auf Haupt-Thread wechseln für Bukkit-API Aufrufe + Bukkit.getScheduler().runTask(NexusLobby.getInstance(), () -> { + HandlerList.unregisterAll(this); + AST.chatInputMode.remove(player.getUniqueId()); + + if (input.equalsIgnoreCase("abbrechen")) { + player.sendTitle("§c§lAbgebrochen", "", 5, 30, 10); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cNamensänderung abgebrochen."); + } else { + // & → § Farb-Codes + String newName = ChatColor.translateAlternateColorCodes('&', input); + armorStand.setCustomName(newName); + armorStand.setCustomNameVisible(!ChatColor.stripColor(newName).isEmpty()); + + player.sendTitle("§a§lName gesetzt!", "§r" + newName, 5, 40, 10); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aName gesetzt: " + newName); + } + + // Zurück zum ArmorStand-Editor + if (armorStand.isValid()) { + new ArmorStandGUI(armorStand, player); + } + }); + } + + // ───────────────────────────────────────────────────────────────────────── + // Logout — Listener sauber abmelden + // ───────────────────────────────────────────────────────────────────────── + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + if (!event.getPlayer().equals(player)) return; + HandlerList.unregisterAll(this); + AST.chatInputMode.remove(player.getUniqueId()); + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCmdExecutor.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCmdExecutor.java index dae4980..00f3e5e 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCmdExecutor.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCmdExecutor.java @@ -27,14 +27,14 @@ import java.util.UUID; */ public class ArmorStandCmdExecutor implements CommandExecutor { - private final String prefix = "§8[§6Nexus§8] "; + private String prefix() { return de.nexuslobby.utils.LangManager.getPrefix(); } @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!(sender instanceof Player p)) return true; if (!p.hasPermission("nexuslobby.armorstand.cmd")) { - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("no_permission")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("no_permission")); return true; } @@ -53,7 +53,7 @@ public class ArmorStandCmdExecutor implements CommandExecutor { case "select4": ArmorStand target = getTargetArmorStand(p); if (target == null) { - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("armorstand_lookat_required")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("armorstand_lookat_required")); return true; } @@ -63,17 +63,17 @@ public class ArmorStandCmdExecutor implements CommandExecutor { UUID targetUUID = target.getUniqueId(); p.setMetadata(metaKey, new FixedMetadataValue(NexusLobby.getInstance(), targetUUID.toString())); - p.sendMessage(prefix + "§aNPC §e" + slotStr + " §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; case "link": if (args.length < 3) { - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("conv_link_usage")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("conv_link_usage")); return true; } if (!p.hasMetadata("conv_npc1") || !p.hasMetadata("conv_npc2")) { - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("conv_mark_npcs")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("conv_mark_npcs")); return true; } @@ -101,8 +101,8 @@ public class ArmorStandCmdExecutor implements CommandExecutor { // Im Manager speichern (Nutzt die erweiterte Methode für Gruppen) NexusLobby.getInstance().getConversationManager().saveLinkExtended(id1, id2, id3, id4, dialogId); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("conv_link_created")); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("conv_link_npcs").replace("{count}", (id3 == null ? "2" : (id4 == null ? "3" : "4")))); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("conv_link_created")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("conv_link_npcs").replace("{count}", (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 @@ -111,14 +111,14 @@ public class ArmorStandCmdExecutor implements CommandExecutor { p.removeMetadata("conv_npc3", NexusLobby.getInstance()); p.removeMetadata("conv_npc4", NexusLobby.getInstance()); } else { - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("conv_speaker_not_found")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("conv_speaker_not_found")); } break; case "unlink": ArmorStand targetUnlink = getTargetArmorStand(p); if (targetUnlink == null) { - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("conv_unlink_lookat")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("conv_unlink_lookat")); return true; } @@ -128,17 +128,17 @@ public class ArmorStandCmdExecutor implements CommandExecutor { // Aus Konfiguration löschen NexusLobby.getInstance().getConversationManager().removeLink(targetUnlink.getUniqueId()); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("conv_unlinked")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("conv_unlinked")); p.spawnParticle(Particle.SMOKE, targetUnlink.getLocation().add(0, 1.0, 0), 20, 0.2, 0.2, 0.2, 0.02); break; case "start": if (args.length < 3) { - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("conv_start_usage")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("conv_start_usage")); return true; } if (!p.hasMetadata("conv_npc1") || !p.hasMetadata("conv_npc2")) { - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("conv_mark_two_npcs")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("conv_mark_two_npcs")); return true; } @@ -163,7 +163,7 @@ public class ArmorStandCmdExecutor implements CommandExecutor { ArmorStand target = getTargetArmorStand(p); if (args[0].equalsIgnoreCase("say") && args.length >= 2) { - if (target == null) { p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("armorstand_lookat_required")); return true; } + if (target == null) { p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("armorstand_lookat_required")); return true; } String text = buildString(args, 1); String colored = ChatColor.translateAlternateColorCodes('&', text); @@ -171,25 +171,25 @@ public class ArmorStandCmdExecutor implements CommandExecutor { // Nutzt die showBubble-Logik aus dem ConversationManager (Ohne Partner-Zwang) NexusLobby.getInstance().getConversationManager().showBubble(target, colored); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("npc_bubble_sent")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("npc_bubble_sent")); return true; } if (args[0].equalsIgnoreCase("lookat")) { - if (target == null) { p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("armorstand_lookat_required")); return true; } + if (target == null) { p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("armorstand_lookat_required")); return true; } if (target.getScoreboardTags().contains("as_lookat")) { target.removeScoreboardTag("as_lookat"); target.setHeadPose(new EulerAngle(0, 0, 0)); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("lookat_off")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("lookat_off")); } else { target.addScoreboardTag("as_lookat"); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("lookat_on")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("lookat_on")); } return true; } if (args[0].equalsIgnoreCase("name") && args.length >= 2) { - if (target == null) { p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("armorstand_lookat_required")); return true; } + if (target == null) { p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("armorstand_lookat_required")); return true; } String nameInput = buildString(args, 1); // Wichtig: Alle alten Namens-Tags entfernen für Konsistenz @@ -198,7 +198,7 @@ public class ArmorStandCmdExecutor implements CommandExecutor { if (nameInput.equalsIgnoreCase("none")) { target.setCustomName(""); target.setCustomNameVisible(false); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("name_removed")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("name_removed")); } else { String colored = ChatColor.translateAlternateColorCodes('&', nameInput); target.setCustomName(colored); @@ -208,19 +208,19 @@ public class ArmorStandCmdExecutor implements CommandExecutor { // ":" wird durch "§§" ersetzt, um Probleme in Scoreboard-Tags zu vermeiden target.addScoreboardTag("as_displayname:" + nameInput.replace(":", "§§")); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("name_set").replace("{name}", colored)); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("status_backup_saved")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("name_set").replace("{name}", colored)); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("status_backup_saved")); } return true; } if (args[0].equalsIgnoreCase("add") && args.length >= 5) { - if (target == null) { p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("armorstand_lookat_required")); return true; } + if (target == null) { p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("armorstand_lookat_required")); return true; } String slot1 = args[1], slot2 = args[2], type = args[3].toLowerCase(); String cmdStr = buildString(args, 4); target.addScoreboardTag("ascmd:" + slot1 + ":" + slot2 + ":" + type + ":" + cmdStr); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("command_bound")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("command_bound")); return true; } @@ -234,18 +234,18 @@ public class ArmorStandCmdExecutor implements CommandExecutor { if (args[0].equalsIgnoreCase("remove")) { if (target == null) { p.sendMessage(de.nexuslobby.utils.LangManager.get("no_target")); return true; } target.getScoreboardTags().removeIf(tag -> tag.startsWith("ascmd:")); - p.sendMessage(prefix + de.nexuslobby.utils.LangManager.get("commands_removed")); + p.sendMessage(prefix() + de.nexuslobby.utils.LangManager.get("commands_removed")); return true; } if (args[0].equalsIgnoreCase("clearbubbles")) { ConversationManager cm = NexusLobby.getInstance().getConversationManager(); if (cm == null) { - p.sendMessage(prefix + "§cConversationManager ist nicht aktiv."); + p.sendMessage(prefix() + "§cConversationManager ist nicht aktiv."); return true; } cm.clearHangingBubbles(); - p.sendMessage(prefix + "§aAlle hängenden Sprechblasen wurden entfernt."); + p.sendMessage(prefix() + "§aAlle hängenden Sprechblasen wurden entfernt."); p.playSound(p.getLocation(), org.bukkit.Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 2f); return true; } diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCommand.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCommand.java index 617a70b..4449e2d 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCommand.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandCommand.java @@ -50,7 +50,7 @@ public class ArmorStandCommand implements CommandExecutor { } // Fester Prefix für maximale Build-Sicherheit - String prefix = "§8[§6Nexus§8] "; + String prefix = de.nexuslobby.utils.LangManager.getPrefix(); if (!p.hasPermission("nexuslobby.armorstand.use")) { p.sendMessage(prefix + ChatColor.RED + "Dazu hast du keine Rechte!"); diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandLookAtModule.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandLookAtModule.java index 10d6d22..6dd05ec 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandLookAtModule.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandLookAtModule.java @@ -1,113 +1,113 @@ -package de.nexuslobby.modules.armorstandtools; - -import de.nexuslobby.NexusLobby; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.ArmorStand; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.util.EulerAngle; -import org.bukkit.util.Vector; - -public class ArmorStandLookAtModule { - - public ArmorStandLookAtModule() { - startRotationTask(); - } - - private void startRotationTask() { - new BukkitRunnable() { - @Override - public void run() { - // Wir gehen alle Welten durch, um markierte ArmorStands zu finden - for (World world : Bukkit.getWorlds()) { - for (ArmorStand as : world.getEntitiesByClass(ArmorStand.class)) { - - // Nur ArmorStands mit dem Tag "as_lookat" - if (as.getScoreboardTags().contains("as_lookat")) { - - // Suche den nächsten Spieler im Umkreis von 7 Blöcken - Player nearest = getNearestPlayer(as, 7.0); - - if (nearest != null) { - // Spieler gefunden -> Kopf zum Spieler drehen - updateLookAt(as, nearest); - } else { - // Kein Spieler da -> Kopf sanft auf 0,0,0 zurücksetzen - resetHeadPose(as); - } - } - } - } - } - }.runTaskTimer(NexusLobby.getInstance(), 0L, 1L); - } - - private void updateLookAt(ArmorStand as, Player player) { - Location asLoc = as.getEyeLocation(); - Location target = player.getEyeLocation(); - - Vector direction = target.toVector().subtract(asLoc.toVector()).normalize(); - - // Ziel-Winkel aus Vektor berechnen - double targetYaw = Math.toDegrees(Math.atan2(-direction.getX(), direction.getZ())); - double targetPitch = Math.toDegrees(Math.asin(direction.getY())); - - // Relativer Yaw zum Körper-Yaw - double relativeYaw = targetYaw - as.getLocation().getYaw(); - relativeYaw = ((relativeYaw + 180) % 360 + 360) % 360 - 180; - - double yawRad = Math.toRadians(relativeYaw); - double pitchRad = Math.toRadians(-targetPitch); - - // Begrenzung (Nacken-Schutz) - if (yawRad > 1.2) yawRad = 1.2; - if (yawRad < -1.2) yawRad = -1.2; - if (pitchRad > 0.8) pitchRad = 0.8; - if (pitchRad < -0.8) pitchRad = -0.8; - - applySmoothPose(as, pitchRad, yawRad); - } - - private void resetHeadPose(ArmorStand as) { - EulerAngle current = as.getHeadPose(); - // Wenn der Kopf schon (fast) gerade ist, nichts tun - if (Math.abs(current.getX()) < 0.01 && Math.abs(current.getY()) < 0.01) { - if (current.getX() != 0 || current.getY() != 0) { - as.setHeadPose(new EulerAngle(0, 0, 0)); - } - return; - } - - // Ziel: 0, 0, 0 (Geradeaus schauen) - applySmoothPose(as, 0, 0); - } - - private void applySmoothPose(ArmorStand as, double pitchRad, double yawRad) { - EulerAngle current = as.getHeadPose(); - double lerp = 0.15; // Geschmeidigkeit - - double finalPitch = current.getX() + (pitchRad - current.getX()) * lerp; - double finalYaw = current.getY() + (yawRad - current.getY()) * lerp; - - as.setHeadPose(new EulerAngle(finalPitch, finalYaw, 0)); - } - - private Player getNearestPlayer(ArmorStand as, double range) { - Player nearest = null; - double dSquared = range * range; - - for (Player p : as.getWorld().getPlayers()) { - // Falls Spieler im Vanish/Spectator ist, ignorieren (optional) - if (p.getGameMode().name().equals("SPECTATOR")) continue; - - double dist = p.getLocation().distanceSquared(as.getLocation()); - if (dist < dSquared) { - dSquared = dist; - nearest = p; - } - } - return nearest; - } +package de.nexuslobby.modules.armorstandtools; + +import de.nexuslobby.NexusLobby; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.EulerAngle; +import org.bukkit.util.Vector; + +public class ArmorStandLookAtModule { + + public ArmorStandLookAtModule() { + startRotationTask(); + } + + private void startRotationTask() { + new BukkitRunnable() { + @Override + public void run() { + // Wir gehen alle Welten durch, um markierte ArmorStands zu finden + for (World world : Bukkit.getWorlds()) { + for (ArmorStand as : world.getEntitiesByClass(ArmorStand.class)) { + + // Nur ArmorStands mit dem Tag "as_lookat" + if (as.getScoreboardTags().contains("as_lookat")) { + + // Suche den nächsten Spieler im Umkreis von 7 Blöcken + Player nearest = getNearestPlayer(as, 7.0); + + if (nearest != null) { + // Spieler gefunden -> Kopf zum Spieler drehen + updateLookAt(as, nearest); + } else { + // Kein Spieler da -> Kopf sanft auf 0,0,0 zurücksetzen + resetHeadPose(as); + } + } + } + } + } + }.runTaskTimer(NexusLobby.getInstance(), 0L, 1L); + } + + private void updateLookAt(ArmorStand as, Player player) { + Location asLoc = as.getEyeLocation(); + Location target = player.getEyeLocation(); + + Vector direction = target.toVector().subtract(asLoc.toVector()).normalize(); + + // Ziel-Winkel aus Vektor berechnen + double targetYaw = Math.toDegrees(Math.atan2(-direction.getX(), direction.getZ())); + double targetPitch = Math.toDegrees(Math.asin(direction.getY())); + + // Relativer Yaw zum Körper-Yaw + double relativeYaw = targetYaw - as.getLocation().getYaw(); + relativeYaw = ((relativeYaw + 180) % 360 + 360) % 360 - 180; + + double yawRad = Math.toRadians(relativeYaw); + double pitchRad = Math.toRadians(-targetPitch); + + // Begrenzung (Nacken-Schutz) + if (yawRad > 1.2) yawRad = 1.2; + if (yawRad < -1.2) yawRad = -1.2; + if (pitchRad > 0.8) pitchRad = 0.8; + if (pitchRad < -0.8) pitchRad = -0.8; + + applySmoothPose(as, pitchRad, yawRad); + } + + private void resetHeadPose(ArmorStand as) { + EulerAngle current = as.getHeadPose(); + // Wenn der Kopf schon (fast) gerade ist, nichts tun + if (Math.abs(current.getX()) < 0.01 && Math.abs(current.getY()) < 0.01) { + if (current.getX() != 0 || current.getY() != 0) { + as.setHeadPose(new EulerAngle(0, 0, 0)); + } + return; + } + + // Ziel: 0, 0, 0 (Geradeaus schauen) + applySmoothPose(as, 0, 0); + } + + private void applySmoothPose(ArmorStand as, double pitchRad, double yawRad) { + EulerAngle current = as.getHeadPose(); + double lerp = 0.15; // Geschmeidigkeit + + double finalPitch = current.getX() + (pitchRad - current.getX()) * lerp; + double finalYaw = current.getY() + (yawRad - current.getY()) * lerp; + + as.setHeadPose(new EulerAngle(finalPitch, finalYaw, 0)); + } + + private Player getNearestPlayer(ArmorStand as, double range) { + Player nearest = null; + double dSquared = range * range; + + for (Player p : as.getWorld().getPlayers()) { + // Falls Spieler im Vanish/Spectator ist, ignorieren (optional) + if (p.getGameMode().name().equals("SPECTATOR")) continue; + + double dist = p.getLocation().distanceSquared(as.getLocation()); + if (dist < dSquared) { + dSquared = dist; + nearest = p; + } + } + return nearest; + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandPoseGUI.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandPoseGUI.java index 0c9c722..45db3a7 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandPoseGUI.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandPoseGUI.java @@ -1,111 +1,111 @@ -package de.nexuslobby.modules.armorstandtools; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.ArmorStand; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.util.EulerAngle; -import java.util.Arrays; - -public class ArmorStandPoseGUI { - - // Schritt 1: Auswahl welches Körperteil (Die "letzte Reihe" ist jetzt hier) - public static void openPartSelectionMenu(Player p, ArmorStand as) { - Inventory inv = Bukkit.createInventory(null, 45, "Pose: Körperteil wählen"); - ItemStack filler = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); - for (int i = 0; i < 45; i++) inv.setItem(i, filler); - - inv.setItem(10, createPartIcon(Material.PLAYER_HEAD, "§eKopf", as.getHeadPose())); - inv.setItem(12, createPartIcon(Material.IRON_CHESTPLATE, "§eTorso", as.getBodyPose())); - inv.setItem(14, createPartIcon(Material.STICK, "§eLinker Arm", as.getLeftArmPose())); - inv.setItem(16, createPartIcon(Material.STICK, "§eRechter Arm", as.getRightArmPose())); - inv.setItem(28, createPartIcon(Material.LEATHER_BOOTS, "§eLinkes Bein", as.getLeftLegPose())); - inv.setItem(30, createPartIcon(Material.LEATHER_BOOTS, "§eRechtes Bein", as.getRightLegPose())); - - inv.setItem(40, createNamedItem(Material.BARRIER, "§cZurück zum Hauptmenü")); - p.openInventory(inv); - } - - // Schritt 2: Detail-Einstellung der Achsen (X, Y, Z) - public static void openAxisDetailMenu(Player p, ArmorStand as, String partName) { - Inventory inv = Bukkit.createInventory(null, 27, "Achsen: " + partName); - ItemStack filler = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); - for (int i = 0; i < 27; i++) inv.setItem(i, filler); - - ArmorStandTool tool = ArmorStandTool.valueOf(partName); - EulerAngle angle = getAngleForPart(as, tool); - - inv.setItem(11, createAxisItem(Material.RED_DYE, "§c§lX-Achse (Pitch)", angle.getX())); - inv.setItem(13, createAxisItem(Material.GREEN_DYE, "§a§lY-Achse (Yaw)", angle.getY())); - inv.setItem(15, createAxisItem(Material.BLUE_DYE, "§b§lZ-Achse (Roll)", angle.getZ())); - - inv.setItem(22, createNamedItem(Material.ARROW, "§7Zurück zur Auswahl")); - p.openInventory(inv); - } - - private static ItemStack createPartIcon(Material mat, String name, EulerAngle angle) { - ItemStack item = new ItemStack(mat); - ItemMeta meta = item.getItemMeta(); - if (meta == null) return item; - meta.setDisplayName(name); - meta.setLore(Arrays.asList( - "§8Rotation:", - "§7X: §f" + Math.round(Math.toDegrees(angle.getX())) + "°", - "§7Y: §f" + Math.round(Math.toDegrees(angle.getY())) + "°", - "§7Z: §f" + Math.round(Math.toDegrees(angle.getZ())) + "°", - "", "§6Klicken zum Justieren" - )); - item.setItemMeta(meta); - return item; - } - - private static ItemStack createAxisItem(Material mat, String name, double angleRad) { - ItemStack item = new ItemStack(mat); - ItemMeta meta = item.getItemMeta(); - if (meta == null) return item; - meta.setDisplayName(name); - meta.setLore(Arrays.asList( - "§7Aktueller Winkel: §f" + Math.round(Math.toDegrees(angleRad)) + "°", - "", - "§eLinksklick: §f+15° §8| §eRechtsklick: §f-15°", - "§6Shift-Klick: §f+1° §8| §6Shift-Rechts: §f-1°" - )); - item.setItemMeta(meta); - return item; - } - - private static ItemStack createNamedItem(Material mat, String name) { - ItemStack item = new ItemStack(mat); - ItemMeta meta = item.getItemMeta(); - if (meta == null) return item; - meta.setDisplayName(name); - item.setItemMeta(meta); - return item; - } - - public static EulerAngle getAngleForPart(ArmorStand as, ArmorStandTool part) { - return switch (part) { - case HEAD_ROT -> as.getHeadPose(); - case BODY_ROT -> as.getBodyPose(); - case L_ARM_ROT -> as.getLeftArmPose(); - case R_ARM_ROT -> as.getRightArmPose(); - case L_LEG_ROT -> as.getLeftLegPose(); - case R_LEG_ROT -> as.getRightLegPose(); - default -> EulerAngle.ZERO; - }; - } - - public static void setAngleForPart(ArmorStand as, ArmorStandTool part, EulerAngle angle) { - switch (part) { - case HEAD_ROT -> as.setHeadPose(angle); - case BODY_ROT -> as.setBodyPose(angle); - case L_ARM_ROT -> as.setLeftArmPose(angle); - case R_ARM_ROT -> as.setRightArmPose(angle); - case L_LEG_ROT -> as.setLeftLegPose(angle); - case R_LEG_ROT -> as.setRightLegPose(angle); - } - } +package de.nexuslobby.modules.armorstandtools; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.util.EulerAngle; +import java.util.Arrays; + +public class ArmorStandPoseGUI { + + // Schritt 1: Auswahl welches Körperteil (Die "letzte Reihe" ist jetzt hier) + public static void openPartSelectionMenu(Player p, ArmorStand as) { + Inventory inv = Bukkit.createInventory(null, 45, "Pose: Körperteil wählen"); + ItemStack filler = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); + for (int i = 0; i < 45; i++) inv.setItem(i, filler); + + inv.setItem(10, createPartIcon(Material.PLAYER_HEAD, "§eKopf", as.getHeadPose())); + inv.setItem(12, createPartIcon(Material.IRON_CHESTPLATE, "§eTorso", as.getBodyPose())); + inv.setItem(14, createPartIcon(Material.STICK, "§eLinker Arm", as.getLeftArmPose())); + inv.setItem(16, createPartIcon(Material.STICK, "§eRechter Arm", as.getRightArmPose())); + inv.setItem(28, createPartIcon(Material.LEATHER_BOOTS, "§eLinkes Bein", as.getLeftLegPose())); + inv.setItem(30, createPartIcon(Material.LEATHER_BOOTS, "§eRechtes Bein", as.getRightLegPose())); + + inv.setItem(40, createNamedItem(Material.BARRIER, "§cZurück zum Hauptmenü")); + p.openInventory(inv); + } + + // Schritt 2: Detail-Einstellung der Achsen (X, Y, Z) + public static void openAxisDetailMenu(Player p, ArmorStand as, String partName) { + Inventory inv = Bukkit.createInventory(null, 27, "Achsen: " + partName); + ItemStack filler = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); + for (int i = 0; i < 27; i++) inv.setItem(i, filler); + + ArmorStandTool tool = ArmorStandTool.valueOf(partName); + EulerAngle angle = getAngleForPart(as, tool); + + inv.setItem(11, createAxisItem(Material.RED_DYE, "§c§lX-Achse (Pitch)", angle.getX())); + inv.setItem(13, createAxisItem(Material.GREEN_DYE, "§a§lY-Achse (Yaw)", angle.getY())); + inv.setItem(15, createAxisItem(Material.BLUE_DYE, "§b§lZ-Achse (Roll)", angle.getZ())); + + inv.setItem(22, createNamedItem(Material.ARROW, "§7Zurück zur Auswahl")); + p.openInventory(inv); + } + + private static ItemStack createPartIcon(Material mat, String name, EulerAngle angle) { + ItemStack item = new ItemStack(mat); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + meta.setDisplayName(name); + meta.setLore(Arrays.asList( + "§8Rotation:", + "§7X: §f" + Math.round(Math.toDegrees(angle.getX())) + "°", + "§7Y: §f" + Math.round(Math.toDegrees(angle.getY())) + "°", + "§7Z: §f" + Math.round(Math.toDegrees(angle.getZ())) + "°", + "", "§6Klicken zum Justieren" + )); + item.setItemMeta(meta); + return item; + } + + private static ItemStack createAxisItem(Material mat, String name, double angleRad) { + ItemStack item = new ItemStack(mat); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + meta.setDisplayName(name); + meta.setLore(Arrays.asList( + "§7Aktueller Winkel: §f" + Math.round(Math.toDegrees(angleRad)) + "°", + "", + "§eLinksklick: §f+15° §8| §eRechtsklick: §f-15°", + "§6Shift-Klick: §f+1° §8| §6Shift-Rechts: §f-1°" + )); + item.setItemMeta(meta); + return item; + } + + private static ItemStack createNamedItem(Material mat, String name) { + ItemStack item = new ItemStack(mat); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + meta.setDisplayName(name); + item.setItemMeta(meta); + return item; + } + + public static EulerAngle getAngleForPart(ArmorStand as, ArmorStandTool part) { + return switch (part) { + case HEAD_ROT -> as.getHeadPose(); + case BODY_ROT -> as.getBodyPose(); + case L_ARM_ROT -> as.getLeftArmPose(); + case R_ARM_ROT -> as.getRightArmPose(); + case L_LEG_ROT -> as.getLeftLegPose(); + case R_LEG_ROT -> as.getRightLegPose(); + default -> EulerAngle.ZERO; + }; + } + + public static void setAngleForPart(ArmorStand as, ArmorStandTool part, EulerAngle angle) { + switch (part) { + case HEAD_ROT -> as.setHeadPose(angle); + case BODY_ROT -> as.setBodyPose(angle); + case L_ARM_ROT -> as.setLeftArmPose(angle); + case R_ARM_ROT -> as.setRightArmPose(angle); + case L_LEG_ROT -> as.setLeftLegPose(angle); + case R_LEG_ROT -> as.setRightLegPose(angle); + } + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandStatusModule.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandStatusModule.java index 5668249..90ccbe8 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandStatusModule.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandStatusModule.java @@ -1,135 +1,135 @@ -package de.nexuslobby.modules.armorstandtools; - -import de.nexuslobby.NexusLobby; -import de.nexuslobby.api.Module; -import de.nexuslobby.modules.servers.ServerChecker; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -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"; - } - - @Override - public void onEnable() { - // Alle 10 Sekunden (200 Ticks) - Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), () -> { - refreshTicks++; - updateAllServerArmorStands(); - }, 100L, 200L); - } - - @Override - 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, forceRefresh); - } - } - } - } - - private void checkAndRefreshStatus(ArmorStand as, boolean forceRefresh) { - String serverName = null; - - for (String tag : as.getScoreboardTags()) { - 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 (serverName == null) return; - - 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; - - // 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) { - String translatedName = ChatColor.translateAlternateColorCodes('&', originalDisplayName); - as.setCustomName(translatedName); - } else { - as.setCustomName("§cOffline"); - } - - // Bedrock-Fix: Sichtbarkeit explizit triggern - as.setCustomNameVisible(true); - - // Status speichern - lastStatus.put(as.getUniqueId(), isOnline); - }); - }); - } - - private String getOriginalName(ArmorStand as) { - for (String tag : as.getScoreboardTags()) { - if (tag.startsWith("as_displayname:")) { - return tag.replace("as_displayname:", "").replace("§§", ":"); - } - } - return null; - } +package de.nexuslobby.modules.armorstandtools; + +import de.nexuslobby.NexusLobby; +import de.nexuslobby.api.Module; +import de.nexuslobby.modules.servers.ServerChecker; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +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"; + } + + @Override + public void onEnable() { + // Alle 10 Sekunden (200 Ticks) + Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), () -> { + refreshTicks++; + updateAllServerArmorStands(); + }, 100L, 200L); + } + + @Override + 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, forceRefresh); + } + } + } + } + + private void checkAndRefreshStatus(ArmorStand as, boolean forceRefresh) { + String serverName = null; + + for (String tag : as.getScoreboardTags()) { + 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 (serverName == null) return; + + 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; + + // 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) { + String translatedName = ChatColor.translateAlternateColorCodes('&', originalDisplayName); + as.setCustomName(translatedName); + } else { + as.setCustomName("§cOffline"); + } + + // Bedrock-Fix: Sichtbarkeit explizit triggern + as.setCustomNameVisible(true); + + // Status speichern + lastStatus.put(as.getUniqueId(), isOnline); + }); + }); + } + + private String getOriginalName(ArmorStand as) { + for (String tag : as.getScoreboardTags()) { + if (tag.startsWith("as_displayname:")) { + return tag.replace("as_displayname:", "").replace("§§", ":"); + } + } + return null; + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandTool.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandTool.java index 088b997..cecf394 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandTool.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandTool.java @@ -59,11 +59,11 @@ public enum ArmorStandTool { // Automatisches Markieren via GUI if (!p.hasMetadata("conv_npc1")) { p.setMetadata("conv_npc1", new FixedMetadataValue(NexusLobby.getInstance(), as.getUniqueId().toString())); - p.sendMessage("§8[§6Nexus§8] §aNPC als Sprecher 1 markiert."); + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aNPC als Sprecher 1 markiert."); } else { p.setMetadata("conv_npc2", new FixedMetadataValue(NexusLobby.getInstance(), as.getUniqueId().toString())); - p.sendMessage("§8[§6Nexus§8] §aNPC als Sprecher 2 markiert."); - p.sendMessage("§8[§6Nexus§8] §7Nutze nun §e/nexuscmd conv start "); + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aNPC als Sprecher 2 markiert."); + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§7Nutze nun §e/nexuscmd conv start "); } p.closeInventory(); } diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java index 7fd9f45..7dfef23 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java @@ -1,297 +1,297 @@ -package de.nexuslobby.modules.armorstandtools; - -import de.nexuslobby.NexusLobby; -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; -import org.bukkit.entity.Entity; -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 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<>(); - // Alle aktiven Bubble-Entities tracken – für zuverlässiges Cleanup bei Reload/Crash - private final Set activeBubbles = 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. - * FIX: Nur ArmorStands mit dem Tag "nexus_bubble" werden entfernt. - * Die alte Bedingung (!isVisible && isMarker && customName != null) war - * zu breit und hätte auch legitime Marker-ArmorStands anderer Systeme gelöscht. - */ - public void clearHangingBubbles() { - // Zuerst: Alle getrackte Bubble-Entities direkt entfernen (schnell, kein World-Scan nötig) - for (UUID uuid : new HashSet<>(activeBubbles)) { - Entity e = Bukkit.getEntity(uuid); - if (e != null) e.remove(); - } - activeBubbles.clear(); - - // Danach: Vollständiger World-Scan als Fallback (fängt Bubbles nach Crash/Reload ab) - int count = 0; - for (World world : Bukkit.getWorlds()) { - for (ArmorStand as : world.getEntitiesByClass(ArmorStand.class)) { - if (as.getScoreboardTags().contains("nexus_bubble")) { - 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()) { - plugin.saveResource("conversations.yml", false); - } - this.config = YamlConfiguration.loadConfiguration(file); - } - - /** - * 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"; - } - - /** - * 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() { - ConfigurationSection links = config.getConfigurationSection("links"); - if (links == null) return; - - for (String npc1String : links.getKeys(false)) { - try { - UUID id1 = UUID.fromString(npc1String); - if (activeSpeakers.contains(id1)) continue; - - ConfigurationSection npcLink = links.getConfigurationSection(npc1String); - if (npcLink == null) continue; - - 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)) { - String finalDialogId = resolveDialogKey(dialogId); - playConversationGroup(partners, finalDialogId); - } - } - } catch (Exception ignored) {} - } - } - - /** - * 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"; - } - if (!config.contains(path + ".dialogue")) { - path = "conversations." + key; - } - - 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; - - @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; - } - - // 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) { - playDynamicSound(as); - showBubble(as, lines.get(step)); - } else { - for (UUID id : participants) activeSpeakers.remove(id); - this.cancel(); - return; - } - step++; - } - }.runTaskTimer(plugin, 0L, 90L); // 4.5s Intervall - } - - /** - * 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); - s.setGravity(false); - s.setCustomName(ChatColorTranslate(text)); - s.setCustomNameVisible(true); - s.setInvulnerable(true); - s.setPersistent(false); // Überlebt keinen Server-Neustart/-Absturz - s.addScoreboardTag("nexus_bubble"); - }); - - activeBubbles.add(bubble.getUniqueId()); - Bukkit.getScheduler().runTaskLater(plugin, () -> { - bubble.remove(); - activeBubbles.remove(bubble.getUniqueId()); - }, 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) { - Bukkit.getLogger().severe("Fehler beim Speichern der Conversations: " + e.getMessage()); - } - } - - public void reload() { - clearHangingBubbles(); // Erst aufräumen, dann neu laden - activeSpeakers.clear(); - setupFile(); - } +package de.nexuslobby.modules.armorstandtools; + +import de.nexuslobby.NexusLobby; +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; +import org.bukkit.entity.Entity; +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 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<>(); + // Alle aktiven Bubble-Entities tracken – für zuverlässiges Cleanup bei Reload/Crash + private final Set activeBubbles = 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. + * FIX: Nur ArmorStands mit dem Tag "nexus_bubble" werden entfernt. + * Die alte Bedingung (!isVisible && isMarker && customName != null) war + * zu breit und hätte auch legitime Marker-ArmorStands anderer Systeme gelöscht. + */ + public void clearHangingBubbles() { + // Zuerst: Alle getrackte Bubble-Entities direkt entfernen (schnell, kein World-Scan nötig) + for (UUID uuid : new HashSet<>(activeBubbles)) { + Entity e = Bukkit.getEntity(uuid); + if (e != null) e.remove(); + } + activeBubbles.clear(); + + // Danach: Vollständiger World-Scan als Fallback (fängt Bubbles nach Crash/Reload ab) + int count = 0; + for (World world : Bukkit.getWorlds()) { + for (ArmorStand as : world.getEntitiesByClass(ArmorStand.class)) { + if (as.getScoreboardTags().contains("nexus_bubble")) { + 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()) { + plugin.saveResource("conversations.yml", false); + } + this.config = YamlConfiguration.loadConfiguration(file); + } + + /** + * 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"; + } + + /** + * 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() { + ConfigurationSection links = config.getConfigurationSection("links"); + if (links == null) return; + + for (String npc1String : links.getKeys(false)) { + try { + UUID id1 = UUID.fromString(npc1String); + if (activeSpeakers.contains(id1)) continue; + + ConfigurationSection npcLink = links.getConfigurationSection(npc1String); + if (npcLink == null) continue; + + 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)) { + String finalDialogId = resolveDialogKey(dialogId); + playConversationGroup(partners, finalDialogId); + } + } + } catch (Exception ignored) {} + } + } + + /** + * 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"; + } + if (!config.contains(path + ".dialogue")) { + path = "conversations." + key; + } + + 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; + + @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; + } + + // 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) { + playDynamicSound(as); + showBubble(as, lines.get(step)); + } else { + for (UUID id : participants) activeSpeakers.remove(id); + this.cancel(); + return; + } + step++; + } + }.runTaskTimer(plugin, 0L, 90L); // 4.5s Intervall + } + + /** + * 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); + s.setGravity(false); + s.setCustomName(ChatColorTranslate(text)); + s.setCustomNameVisible(true); + s.setInvulnerable(true); + s.setPersistent(false); // Überlebt keinen Server-Neustart/-Absturz + s.addScoreboardTag("nexus_bubble"); + }); + + activeBubbles.add(bubble.getUniqueId()); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + bubble.remove(); + activeBubbles.remove(bubble.getUniqueId()); + }, 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) { + Bukkit.getLogger().severe("Fehler beim Speichern der Conversations: " + e.getMessage()); + } + } + + public void reload() { + clearHangingBubbles(); // Erst aufräumen, dann neu laden + activeSpeakers.clear(); + setupFile(); + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/DynamicArmorStandModule.java b/src/main/java/de/nexuslobby/modules/armorstandtools/DynamicArmorStandModule.java index 21ba714..75a4800 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/DynamicArmorStandModule.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/DynamicArmorStandModule.java @@ -1,131 +1,131 @@ -package de.nexuslobby.modules.armorstandtools; - -import de.nexuslobby.NexusLobby; -import de.nexuslobby.api.Module; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.Particle; -import org.bukkit.World; -import org.bukkit.entity.ArmorStand; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.persistence.PersistentDataType; -import org.bukkit.util.EulerAngle; - -public class DynamicArmorStandModule implements Module { - - private final NamespacedKey npcKey = new NamespacedKey(NexusLobby.getInstance(), "dynamic_npc"); - - @Override - public String getName() { - return "DynamicArmorStands"; - } - - @Override - public void onEnable() { - startUpdateTask(); - } - - private void startUpdateTask() { - // Alle 5 Ticks (0.25s) für flüssige Partikel - Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), this::updateAllArmorStands, 20L, 5L); - } - - public void updateAllArmorStands() { - for (World world : Bukkit.getWorlds()) { - long time = world.getTime(); - boolean isIngameNight = (time >= 13000 && time <= 23000); - - for (Entity entity : world.getEntities()) { - if (entity instanceof ArmorStand as) { - if (as.getPersistentDataContainer().has(npcKey, PersistentDataType.BYTE) || - as.getScoreboardTags().contains("as_dynamic")) { - - applyDynamicChanges(as, isIngameNight); - - if (isIngameNight && as.getEquipment().getItemInOffHand().getType() == Material.TORCH) { - spawnStarParticles(as); - } - } - } - } - } - } - - private void applyDynamicChanges(ArmorStand as, boolean isNight) { - if (!as.hasArms()) as.setArms(true); - - // --- SPIELER-NÄHE --- - boolean playerNearby = false; - for (Entity nearby : as.getNearbyEntities(2.0, 2.0, 2.0)) { - if (nearby instanceof Player) { playerNearby = true; break; } - } - - if (playerNearby) { - if (as.getEquipment().getItemInMainHand().getType() != Material.STONE_SWORD) { - as.getEquipment().setItemInMainHand(new ItemStack(Material.STONE_SWORD)); - as.setRightArmPose(new EulerAngle(Math.toRadians(-80), Math.toRadians(-10), 0)); - } - } else { - if (as.getEquipment().getItemInMainHand().getType() == Material.STONE_SWORD) { - as.getEquipment().setItemInMainHand(null); - as.setRightArmPose(EulerAngle.ZERO); - } - } - - // --- INGAME-ZEIT --- - if (isNight) { - if (as.getEquipment().getItemInOffHand().getType() != Material.TORCH) { - as.getEquipment().setItemInOffHand(new ItemStack(Material.TORCH)); - as.setLeftArmPose(new EulerAngle(Math.toRadians(-50), Math.toRadians(15), 0)); - } - } else { - if (as.getEquipment().getItemInOffHand().getType() == Material.TORCH) { - as.getEquipment().setItemInOffHand(null); - as.setLeftArmPose(EulerAngle.ZERO); - } - } - } - - private void spawnStarParticles(ArmorStand as) { - Location loc = as.getLocation(); - double yawRad = Math.toRadians(loc.getYaw()); - - // Vektoren für Blickrichtung und Seite - double dirX = -Math.sin(yawRad); - double dirZ = Math.cos(yawRad); - double sideX = Math.cos(yawRad); - double sideZ = Math.sin(yawRad); - - // Position: Vorne (0.4), Links (0.5), Oben (1.45) - double finalX = loc.getX() + (dirX * 0.8) + (sideX * 0.2); - double finalY = loc.getY() + 1.45; - double finalZ = loc.getZ() + (dirZ * 0.8) + (sideZ * 0.2); - - Location particleLoc = new Location(as.getWorld(), finalX, finalY, finalZ); - as.getWorld().spawnParticle(Particle.WAX_OFF, particleLoc, 2, 0.05, 0.05, 0.05, 0.01); - } - - public void toggleDynamicStatus(ArmorStand as) { - if (as.getPersistentDataContainer().has(npcKey, PersistentDataType.BYTE)) { - as.getPersistentDataContainer().remove(npcKey); - as.removeScoreboardTag("as_dynamic"); - as.getEquipment().setItemInMainHand(null); - as.getEquipment().setItemInOffHand(null); - as.setRightArmPose(EulerAngle.ZERO); - as.setLeftArmPose(EulerAngle.ZERO); - } else { - as.getPersistentDataContainer().set(npcKey, PersistentDataType.BYTE, (byte) 1); - as.addScoreboardTag("as_dynamic"); - applyDynamicChanges(as, (as.getWorld().getTime() >= 13000 && as.getWorld().getTime() <= 23000)); - } - } - - @Override - public void onDisable() { - // Bereinigung falls nötig - } +package de.nexuslobby.modules.armorstandtools; + +import de.nexuslobby.NexusLobby; +import de.nexuslobby.api.Module; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Particle; +import org.bukkit.World; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.EulerAngle; + +public class DynamicArmorStandModule implements Module { + + private final NamespacedKey npcKey = new NamespacedKey(NexusLobby.getInstance(), "dynamic_npc"); + + @Override + public String getName() { + return "DynamicArmorStands"; + } + + @Override + public void onEnable() { + startUpdateTask(); + } + + private void startUpdateTask() { + // Alle 5 Ticks (0.25s) für flüssige Partikel + Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), this::updateAllArmorStands, 20L, 5L); + } + + public void updateAllArmorStands() { + for (World world : Bukkit.getWorlds()) { + long time = world.getTime(); + boolean isIngameNight = (time >= 13000 && time <= 23000); + + for (Entity entity : world.getEntities()) { + if (entity instanceof ArmorStand as) { + if (as.getPersistentDataContainer().has(npcKey, PersistentDataType.BYTE) || + as.getScoreboardTags().contains("as_dynamic")) { + + applyDynamicChanges(as, isIngameNight); + + if (isIngameNight && as.getEquipment().getItemInOffHand().getType() == Material.TORCH) { + spawnStarParticles(as); + } + } + } + } + } + } + + private void applyDynamicChanges(ArmorStand as, boolean isNight) { + if (!as.hasArms()) as.setArms(true); + + // --- SPIELER-NÄHE --- + boolean playerNearby = false; + for (Entity nearby : as.getNearbyEntities(2.0, 2.0, 2.0)) { + if (nearby instanceof Player) { playerNearby = true; break; } + } + + if (playerNearby) { + if (as.getEquipment().getItemInMainHand().getType() != Material.STONE_SWORD) { + as.getEquipment().setItemInMainHand(new ItemStack(Material.STONE_SWORD)); + as.setRightArmPose(new EulerAngle(Math.toRadians(-80), Math.toRadians(-10), 0)); + } + } else { + if (as.getEquipment().getItemInMainHand().getType() == Material.STONE_SWORD) { + as.getEquipment().setItemInMainHand(null); + as.setRightArmPose(EulerAngle.ZERO); + } + } + + // --- INGAME-ZEIT --- + if (isNight) { + if (as.getEquipment().getItemInOffHand().getType() != Material.TORCH) { + as.getEquipment().setItemInOffHand(new ItemStack(Material.TORCH)); + as.setLeftArmPose(new EulerAngle(Math.toRadians(-50), Math.toRadians(15), 0)); + } + } else { + if (as.getEquipment().getItemInOffHand().getType() == Material.TORCH) { + as.getEquipment().setItemInOffHand(null); + as.setLeftArmPose(EulerAngle.ZERO); + } + } + } + + private void spawnStarParticles(ArmorStand as) { + Location loc = as.getLocation(); + double yawRad = Math.toRadians(loc.getYaw()); + + // Vektoren für Blickrichtung und Seite + double dirX = -Math.sin(yawRad); + double dirZ = Math.cos(yawRad); + double sideX = Math.cos(yawRad); + double sideZ = Math.sin(yawRad); + + // Position: Vorne (0.4), Links (0.5), Oben (1.45) + double finalX = loc.getX() + (dirX * 0.8) + (sideX * 0.2); + double finalY = loc.getY() + 1.45; + double finalZ = loc.getZ() + (dirZ * 0.8) + (sideZ * 0.2); + + Location particleLoc = new Location(as.getWorld(), finalX, finalY, finalZ); + as.getWorld().spawnParticle(Particle.WAX_OFF, particleLoc, 2, 0.05, 0.05, 0.05, 0.01); + } + + public void toggleDynamicStatus(ArmorStand as) { + if (as.getPersistentDataContainer().has(npcKey, PersistentDataType.BYTE)) { + as.getPersistentDataContainer().remove(npcKey); + as.removeScoreboardTag("as_dynamic"); + as.getEquipment().setItemInMainHand(null); + as.getEquipment().setItemInOffHand(null); + as.setRightArmPose(EulerAngle.ZERO); + as.setLeftArmPose(EulerAngle.ZERO); + } else { + as.getPersistentDataContainer().set(npcKey, PersistentDataType.BYTE, (byte) 1); + as.addScoreboardTag("as_dynamic"); + applyDynamicChanges(as, (as.getWorld().getTime() >= 13000 && as.getWorld().getTime() <= 23000)); + } + } + + @Override + public void onDisable() { + // Bereinigung falls nötig + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/ball/SoccerModule.java b/src/main/java/de/nexuslobby/modules/ball/SoccerModule.java index 9850a17..5ee99cb 100644 --- a/src/main/java/de/nexuslobby/modules/ball/SoccerModule.java +++ b/src/main/java/de/nexuslobby/modules/ball/SoccerModule.java @@ -1,795 +1,795 @@ -package de.nexuslobby.modules.ball; - -import de.nexuslobby.NexusLobby; -import de.nexuslobby.api.Module; -import org.bukkit.*; -import org.bukkit.block.Block; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.entity.ArmorStand; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.player.PlayerInteractAtEntityEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.SkullMeta; -import org.bukkit.profile.PlayerProfile; -import org.bukkit.util.Vector; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.*; - -public class SoccerModule implements Module, Listener, CommandExecutor { - - // ========================================================================= - // KONSTANTEN - // ========================================================================= - private static final String TEXTURE_URL = - "http://textures.minecraft.net/texture/451f8cfcfb85d77945dc6a3618414093e70436b46d2577b28c727f1329b7265e"; - private static final String BALL_TAG = "nexusball_entity"; - private static final String BALL_NAME = "NexusBall"; - - private static final double DRIBBLE_DETECTION_RADIUS = 0.7; - private static final double DRIBBLE_HEIGHT = 0.5; - private static final double DRIBBLE_FORCE = 0.35; - private static final double DRIBBLE_LIFT = 0.12; - private static final double KICK_FORCE = 1.35; - private static final double KICK_LIFT = 0.38; - private static final double WALL_BOUNCE_DAMPING = 0.75; - private static final double WALL_CHECK_DISTANCE = 1.3; - private static final double VOID_THRESHOLD = -5.0; - private static final double CLEANUP_RADIUS = 5.0; - - // Verhindert dass Dribbling einen Kick sofort überschreibt. - private static final long KICK_DRIBBLE_LOCK_MS = 300L; - - private static final double PARTICLE_SPEED_HIGH = 0.85; - private static final double PARTICLE_SPEED_MEDIUM = 0.45; - private static final double PARTICLE_SPEED_MIN = 0.05; - - // Tor-Erkennung: Schrittweite beim Segment-Check in Blöcken. - // 0.1 → bei 1.35 b/t Kick-Speed ~14 Prüfpunkte pro Tick. Sehr sicher. - private static final double CHECK_STEP = 0.1; - - // ── Tor-Partikel ────────────────────────────────────────────────────────── - private static final Particle.DustOptions DUST_TEAM1 = new Particle.DustOptions(Color.fromRGB(30, 120, 255), 1.2f); - private static final Particle.DustOptions DUST_TEAM2 = new Particle.DustOptions(Color.fromRGB(255, 50, 50), 1.2f); - private static final Particle.DustOptions DUST_GOAL_FLASH = new Particle.DustOptions(Color.YELLOW, 2.0f); - private static final double PARTICLE_STEP = 0.35; - private static final long GOAL_RESPAWN_DELAY = 80L; - private static final long GOAL_COOLDOWN_MS = 3_000L; - - // ========================================================================= - // LAUFZEIT-ZUSTAND - // ========================================================================= - private ArmorStand ball; - private Location spawnLocation; - private long lastMoveTime; - private long lastGoalTime = 0L; - private long lastKickTime = 0L; - - // ── Tor-Erkennung: Position aus dem VORHERIGEN Tick ─────────────────────── - // - // WARUM DAS FUNKTIONIERT: - // getLocation() eines ArmorStands liefert in Tick N die Position aus Tick N-1. - // Das klingt wie ein Problem, ist aber tatsächlich die Lösung: - // - // Wenn wir in jedem Tick: - // 1. prevPos = die aktuelle getLocation() merken (= echte Position aus letztem Tick) - // 2. Am ENDE des Ticks: currPos = getLocation() lesen - // 3. Das Segment prevPos→currPos prüfen - // - // Dann deckt dieses Segment exakt den Weg ab den der Ball in Minecraft - // in den letzten 50ms zurückgelegt hat – egal wie schnell, egal ob Kick - // oder Dribble oder geschoben. - // - // WICHTIG: prevPos wird am ANFANG des Ticks gesetzt (bevor Physik-Berechnungen - // die Velocity ändern), currPos am ENDE. So ist das Segment vollständig. - private Location prevPos = null; - - private final Map goals = new LinkedHashMap<>(); - private final Map scores = new HashMap<>(); - private final Map selPos1 = new HashMap<>(); - private final Map selPos2 = new HashMap<>(); - - // ========================================================================= - // MODULE-LIFECYCLE - // ========================================================================= - - @Override public String getName() { return "Soccer"; } - - @Override - public void onEnable() { - Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); - loadConfigLocation(); - loadGoals(); - scores.put(1, 0); - scores.put(2, 0); - - removeAllOldBalls(); - Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { - removeAllOldBalls(); - spawnBall(); - }, 40L); - Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), this::removeAllOldBalls, 100L); - - // ── Physik-Tick ──────────────────────────────────────────────────────── - Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), () -> { - if (ball != null && ball.isValid()) removeDuplicateBalls(); - if (ball == null || !ball.isValid()) return; - - // ── SCHRITT 1: Aktuelle Position als "vorherige" speichern ───────── - // getLocation() ist hier die Position aus dem letzten Server-Tick – - // d.h. die Position BEVOR die Physik dieses Ticks berechnet wurde. - Location currentPos = ball.getLocation().clone(); - - // ── SCHRITT 2: Physik und Spieler-Interaktion verarbeiten ────────── - Vector vel = ball.getVelocity(); - double speed = vel.length(); - handleWallBounce(vel); - handleParticles(speed); - handleDribbling(); - - // ── SCHRITT 3: Nach Physik die neue Position lesen ───────────────── - // Minecraft wendet nach setVelocity noch Drag (0.98 horizontal, Gravity ~0.08 - // + 0.98 vertikal) an. Wir approximieren: newPos = currentPos + vel * 0.98 - // Das ist deutlich genauer als currentPos + vel und vermeidet 1-Tick-Versatz - // bei der Tor-Erkennung nach schnellen Kicks. - Location newPos = currentPos.clone().add(vel.clone().multiply(0.98)); - - // ── SCHRITT 4: Segment prüfen ───────────────────────────────────── - // Das Segment prevPos → newPos deckt den vollständigen Weg ab den - // der Ball seit dem letzten Tor-Check zurückgelegt hat. - if (!goals.isEmpty() - && System.currentTimeMillis() - lastGoalTime >= GOAL_COOLDOWN_MS - && prevPos != null) { - checkSegment(prevPos, newPos, currentPos.getWorld()); - } - - // ── SCHRITT 5: prevPos für nächsten Tick aktualisieren ───────────── - prevPos = newPos.clone(); - - // ── Respawn-Check ────────────────────────────────────────────────── - long respawnDelayMs = NexusLobby.getInstance().getConfig() - .getLong("ball.respawn_delay", 60) * 1_000L; - if (System.currentTimeMillis() - lastMoveTime > respawnDelayMs - || ball.getLocation().getY() < VOID_THRESHOLD) { - respawnBall(); - } - }, 1L, 1L); - - // ── Tor-Partikel ────────────────────────────────────────────────────── - Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), - this::drawAllGoalParticles, 4L, 4L); - } - - @Override - public void onDisable() { - org.bukkit.event.HandlerList.unregisterAll(this); - if (ball != null && ball.isValid()) { ball.remove(); ball = null; } - } - - // ========================================================================= - // TOR-ERKENNUNG - // ========================================================================= - - /** - * Prüft das Segment von {@code from} nach {@code to} auf Tor-Treffer. - * - * Das Segment wird in CHECK_STEP (0.1 Block) Schritte unterteilt. - * An jedem Punkt werden drei Y-Höhen geprüft (Fuß / Mitte / Kopf des Balls). - * - * @param from Position am Anfang des Ticks - * @param to Position am Ende des Ticks (= from + velocity) - * @param world Welt des Balls - */ - private void checkSegment(Location from, Location to, World world) { - if (world == null) return; - - double dx = to.getX() - from.getX(); - double dy = to.getY() - from.getY(); - double dz = to.getZ() - from.getZ(); - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - - // Ball steht fast still → nur aktuellen Punkt prüfen - if (len < 0.001) { - checkPoint(from.getX(), from.getY(), from.getZ(), world); - return; - } - - int steps = Math.max(1, (int) Math.ceil(len / CHECK_STEP)); - double sx = dx / steps; - double sy = dy / steps; - double sz = dz / steps; - - for (int i = 0; i <= steps; i++) { - double x = from.getX() + sx * i; - double y = from.getY() + sy * i; - double z = from.getZ() + sz * i; - if (checkPoint(x, y, z, world)) return; - } - } - - /** - * Prüft einen Punkt in drei Y-Höhen gegen alle Tore. - * @return true wenn ein Tor gewertet wurde (Schleife soll abbrechen) - */ - private boolean checkPoint(double x, double y, double z, World world) { - for (double yOff : new double[]{0.0, 0.45, 0.9}) { - Location check = new Location(world, x, y + yOff, z); - for (SoccerGoal goal : goals.values()) { - if (goal.contains(check)) { - scoreGoal(goal); - return true; - } - } - } - return false; - } - - // ========================================================================= - // TOR WERTEN - // ========================================================================= - - private void scoreGoal(SoccerGoal goal) { - if (System.currentTimeMillis() - lastGoalTime < GOAL_COOLDOWN_MS) return; - lastGoalTime = System.currentTimeMillis(); - scores.merge(goal.team, 1, Integer::sum); - - String teamColor = goal.team == 1 ? "§9" : "§c"; - String teamName = goal.team == 1 ? "Team Blau" : "Team Rot"; - - if (ball != null && ball.isValid()) ball.setVelocity(new Vector(0, 0, 0)); - prevPos = null; // Reset damit kein Doppel-Trigger aus altem Segment - - for (Player p : Bukkit.getOnlinePlayers()) { - p.sendTitle(teamColor + "§lTOR!", teamColor + teamName + " §8| §fStand: " + getScoreString(), 5, 50, 15); - p.sendMessage("§8[§6Soccer§8] " + teamColor + "§lTOR! §r§7für " + teamName + " §8| §fStand: " + getScoreString()); - p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1f); - } - - spawnGoalCelebration(goal); - Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), this::respawnBall, GOAL_RESPAWN_DELAY); - } - - private void spawnGoalCelebration(SoccerGoal goal) { - Location center = goal.getCenter(); - World world = center.getWorld(); - if (world == null) return; - for (int wave = 0; wave < 6; wave++) { - final double offsetY = wave * 0.25; - Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { - Location wLoc = center.clone().add(0, offsetY, 0); - world.spawnParticle(Particle.DUST, wLoc, 50, 1.2, 0.6, 1.2, 0, DUST_GOAL_FLASH); - world.spawnParticle(Particle.FIREWORK, wLoc, 25, 1.0, 0.8, 1.0, 0.25); - world.spawnParticle(Particle.EXPLOSION, center, 2, 0.4, 0.4, 0.4, 0.05); - }, wave * 7L); - } - } - - // ========================================================================= - // PHYSIK - // ========================================================================= - - private void handleDribbling() { - if (ball == null || !ball.isValid()) return; - if (System.currentTimeMillis() - lastKickTime < KICK_DRIBBLE_LOCK_MS) return; - for (Entity nearby : ball.getNearbyEntities(DRIBBLE_DETECTION_RADIUS, DRIBBLE_HEIGHT, DRIBBLE_DETECTION_RADIUS)) { - if (nearby instanceof Player player) { - Vector dir = ball.getLocation().toVector().subtract(player.getLocation().toVector()); - if (dir.lengthSquared() > 0) { - dir.normalize().setY(DRIBBLE_LIFT); - ball.setVelocity(dir.multiply(DRIBBLE_FORCE)); - lastMoveTime = System.currentTimeMillis(); - } - } - } - } - - private void handleWallBounce(Vector vel) { - // Nur prüfen wenn genug horizontale Bewegung vorhanden ist. - // Y-Komponente absichtlich ignoriert – der Bounce gilt nur für Wände (X/Z), - // nicht für Boden/Decke. So verhindert man dass liegende Boden-Blöcke - // fälschlicherweise als Wand erkannt werden und den Ball dauerhaft blockieren. - double hSpeed = vel.getX() * vel.getX() + vel.getZ() * vel.getZ(); - if (hSpeed < 0.001) return; - - Location loc = ball.getLocation(); - // Y + 0.5 anheben damit wir auf Wand-Höhe prüfen, nicht auf Boden-Ebene. - // Ohne den Offset würde loc.getY() (Boden des ArmorStands) den Bodenblock selbst treffen. - Block nx = loc.clone().add(vel.getX() * WALL_CHECK_DISTANCE, 0.5, 0).getBlock(); - Block nz = loc.clone().add(0, 0.5, vel.getZ() * WALL_CHECK_DISTANCE).getBlock(); - boolean bounced = false; - if (nx.getType().isSolid()) { vel.setX(-vel.getX() * WALL_BOUNCE_DAMPING); bounced = true; } - if (nz.getType().isSolid()) { vel.setZ(-vel.getZ() * WALL_BOUNCE_DAMPING); bounced = true; } - if (bounced) { - ball.setVelocity(vel); - ball.getWorld().playSound(loc, Sound.BLOCK_WOOD_BREAK, 0.6f, 1.3f); - } - } - - private void handleParticles(double speed) { - if (speed < PARTICLE_SPEED_MIN) return; - Location loc = ball.getLocation().add(0, 0.2, 0); - World w = loc.getWorld(); - if (w == null) return; - if (speed > PARTICLE_SPEED_HIGH) w.spawnParticle(Particle.SONIC_BOOM, loc, 1, 0, 0, 0, 0); - else if (speed > PARTICLE_SPEED_MEDIUM) w.spawnParticle(Particle.CRIT, loc, 3, 0.1, 0.1, 0.1, 0.08); - else w.spawnParticle(Particle.SMOKE, loc, 1, 0.05, 0, 0.05, 0.02); - } - - // ========================================================================= - // TOR-PARTIKEL ZEICHNEN - // ========================================================================= - - private void drawAllGoalParticles() { - for (SoccerGoal goal : goals.values()) drawGoalFrame(goal); - } - - private void drawGoalFrame(SoccerGoal goal) { - World world = goal.getWorld(); - if (world == null) return; - Particle.DustOptions dust = goal.team == 1 ? DUST_TEAM1 : DUST_TEAM2; - - double x1 = Math.min(goal.pos1.getX(), goal.pos2.getX()), x2 = Math.max(goal.pos1.getX(), goal.pos2.getX()); - double y1 = Math.min(goal.pos1.getY(), goal.pos2.getY()), y2 = Math.max(goal.pos1.getY(), goal.pos2.getY()); - double z1 = Math.min(goal.pos1.getZ(), goal.pos2.getZ()), z2 = Math.max(goal.pos1.getZ(), goal.pos2.getZ()); - - line(world, x1,y1,z1, x2,y1,z1, dust); line(world, x1,y1,z2, x2,y1,z2, dust); - line(world, x1,y1,z1, x1,y1,z2, dust); line(world, x2,y1,z1, x2,y1,z2, dust); - line(world, x1,y2,z1, x2,y2,z1, dust); line(world, x1,y2,z2, x2,y2,z2, dust); - line(world, x1,y2,z1, x1,y2,z2, dust); line(world, x2,y2,z1, x2,y2,z2, dust); - line(world, x1,y1,z1, x1,y2,z1, dust); line(world, x2,y1,z1, x2,y2,z1, dust); - line(world, x1,y1,z2, x1,y2,z2, dust); line(world, x2,y1,z2, x2,y2,z2, dust); - } - - private void line(World world, double ax, double ay, double az, - double bx, double by, double bz, Particle.DustOptions dust) { - double dx = bx-ax, dy = by-ay, dz = bz-az; - double len = Math.sqrt(dx*dx + dy*dy + dz*dz); - if (len < 0.01) return; - int steps = (int) Math.ceil(len / PARTICLE_STEP); - dx /= steps; dy /= steps; dz /= steps; - for (int i = 0; i <= steps; i++) - world.spawnParticle(Particle.DUST, ax+dx*i, ay+dy*i, az+dz*i, 1, 0,0,0,0, dust, true); - } - - // ========================================================================= - // DATEN-KLASSE: SoccerGoal - // ========================================================================= - - private static class SoccerGoal { - final String name; final Location pos1, pos2; final int team; - - SoccerGoal(String name, Location pos1, Location pos2, int team) { - this.name = name; this.pos1 = pos1; this.pos2 = pos2; this.team = team; - } - - boolean contains(Location loc) { - if (loc.getWorld() == null || !loc.getWorld().equals(pos1.getWorld())) return false; - return loc.getX() >= Math.min(pos1.getX(), pos2.getX()) - && loc.getX() <= Math.max(pos1.getX(), pos2.getX()) - && loc.getY() >= Math.min(pos1.getY(), pos2.getY()) - && loc.getY() <= Math.max(pos1.getY(), pos2.getY()) - && loc.getZ() >= Math.min(pos1.getZ(), pos2.getZ()) - && loc.getZ() <= Math.max(pos1.getZ(), pos2.getZ()); - } - - Location getCenter() { - return new Location(pos1.getWorld(), - (pos1.getX()+pos2.getX())/2.0, - (pos1.getY()+pos2.getY())/2.0, - (pos1.getZ()+pos2.getZ())/2.0); - } - - World getWorld() { return pos1.getWorld(); } - } - - // ========================================================================= - // CONFIG: TORE - // ========================================================================= - - private void loadGoals() { - goals.clear(); - FileConfiguration config = NexusLobby.getInstance().getConfig(); - ConfigurationSection section = config.getConfigurationSection("ball.goals"); - if (section == null) return; - for (String name : section.getKeys(false)) { - String path = "ball.goals." + name; - Location pos1 = loadLoc(config, path + ".pos1"); - Location pos2 = loadLoc(config, path + ".pos2"); - if (pos1 == null || pos2 == null) continue; - goals.put(name, new SoccerGoal(name, pos1, pos2, config.getInt(path + ".team", 1))); - } - Bukkit.getLogger().info("[Soccer] " + goals.size() + " Tor(e) geladen."); - } - - private void saveGoal(SoccerGoal g) { - String path = "ball.goals." + g.name; - FileConfiguration c = NexusLobby.getInstance().getConfig(); - saveLoc(c, path + ".pos1", g.pos1); saveLoc(c, path + ".pos2", g.pos2); - c.set(path + ".team", g.team); - NexusLobby.getInstance().saveConfig(); - } - - private void deleteGoal(String name) { - NexusLobby.getInstance().getConfig().set("ball.goals." + name, null); - NexusLobby.getInstance().saveConfig(); - } - - private static void saveLoc(FileConfiguration c, String path, Location loc) { - if (loc == null || loc.getWorld() == null) return; - c.set(path + ".world", loc.getWorld().getName()); - c.set(path + ".x", loc.getX()); c.set(path + ".y", loc.getY()); c.set(path + ".z", loc.getZ()); - } - - private static Location loadLoc(FileConfiguration c, String path) { - String wName = c.getString(path + ".world"); - if (wName == null) return null; - World world = Bukkit.getWorld(wName); - if (world == null) { Bukkit.getLogger().warning("[Soccer] Welt nicht gefunden: " + wName); return null; } - return new Location(world, c.getDouble(path + ".x"), c.getDouble(path + ".y"), c.getDouble(path + ".z")); - } - - // ========================================================================= - // BALL SPAWNEN / RESPAWNEN - // ========================================================================= - - private void spawnBall() { - if (spawnLocation == null || spawnLocation.getWorld() == null) return; - if (ball != null && ball.isValid()) return; - ball = (ArmorStand) spawnLocation.getWorld().spawnEntity(spawnLocation.clone(), EntityType.ARMOR_STAND); - ball.setInvisible(true); ball.setGravity(true); ball.setBasePlate(false); - ball.setSmall(true); ball.setInvulnerable(false); ball.setArms(false); - ball.setCustomNameVisible(false); ball.setCustomName(BALL_NAME); - ball.setPersistent(true); ball.addScoreboardTag(BALL_TAG); - if (ball.getEquipment() != null) ball.getEquipment().setHelmet(getSoccerHead()); - prevPos = spawnLocation.clone(); - lastMoveTime = System.currentTimeMillis(); - } - - public void respawnBall() { - if (ball != null && ball.isValid()) { ball.remove(); ball = null; } - removeAllOldBalls(); - prevPos = null; - spawnBall(); - } - - private ItemStack getSoccerHead() { - ItemStack head = new ItemStack(Material.PLAYER_HEAD); - SkullMeta meta = (SkullMeta) head.getItemMeta(); - if (meta == null) return head; - PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID(), BALL_NAME); - try { profile.getTextures().setSkin(new URL(TEXTURE_URL)); } - catch (MalformedURLException e) { Bukkit.getLogger().warning("[NexusLobby] Ungültige Ball-Textur URL!"); } - meta.setOwnerProfile(profile); - head.setItemMeta(meta); - return head; - } - - // ========================================================================= - // CLEANUP - // ========================================================================= - - private void removeDuplicateBalls() { - if (ball == null || !ball.isValid() || ball.getLocation().getWorld() == null) return; - for (Entity e : ball.getLocation().getWorld().getNearbyEntities(ball.getLocation(), 50, 50, 50)) { - if (e instanceof ArmorStand s && isBallEntity(s) && !s.getUniqueId().equals(ball.getUniqueId())) - s.remove(); - } - } - - private void removeAllOldBalls() { - int removed = 0; - if (spawnLocation != null && spawnLocation.getWorld() != null) - removed = cleanupWorld(spawnLocation.getWorld()); - else - for (World w : Bukkit.getWorlds()) removed += cleanupWorld(w); - if (removed > 0) Bukkit.getLogger().info("[NexusLobby] " + removed + " alte Ball-Entities entfernt."); - } - - private int cleanupWorld(World world) { - int n = 0; - for (Entity e : world.getEntities()) { - if (e instanceof ArmorStand s && isBallEntity(s) - && (ball == null || !s.getUniqueId().equals(ball.getUniqueId()))) { - s.remove(); n++; - } - } - return n; - } - - private boolean isBallEntity(ArmorStand s) { - if (s.getScoreboardTags().contains(BALL_TAG)) return true; - if (BALL_NAME.equals(s.getCustomName())) return true; - if (s.getEquipment() != null && s.getEquipment().getHelmet() != null) { - ItemStack h = s.getEquipment().getHelmet(); - if (h.getType() == Material.PLAYER_HEAD && h.hasItemMeta()) { - SkullMeta m = (SkullMeta) h.getItemMeta(); - if (m.hasOwner() && m.getOwnerProfile() != null - && BALL_NAME.equals(m.getOwnerProfile().getName())) return true; - } - } - if (spawnLocation != null && spawnLocation.getWorld() != null - && s.getWorld().equals(spawnLocation.getWorld()) - && s.isSmall() && s.isInvisible() && !s.hasBasePlate() - && s.getLocation().distance(spawnLocation) < CLEANUP_RADIUS - && s.getEquipment() != null && s.getEquipment().getHelmet() != null - && s.getEquipment().getHelmet().getType() == Material.PLAYER_HEAD) - return true; - return false; - } - - // ========================================================================= - // EVENTS - // ========================================================================= - - @EventHandler - public void onBallPunch(EntityDamageByEntityEvent event) { - if (ball == null || !event.getEntity().equals(ball)) return; - event.setCancelled(true); - if (!(event.getDamager() instanceof Player p)) return; - - Vector dir = p.getLocation().getDirection().normalize(); - dir.multiply(KICK_FORCE).setY(KICK_LIFT); - - // prevPos auf aktuelle Position setzen bevor der Ball sich bewegt – - // so startet das nächste Segment am richtigen Punkt. - prevPos = ball.getLocation().clone(); - lastKickTime = System.currentTimeMillis(); - ball.setVelocity(dir); - - ball.getWorld().playSound(ball.getLocation(), Sound.ENTITY_ZOMBIE_ATTACK_IRON_DOOR, 0.6f, 1.5f); - lastMoveTime = System.currentTimeMillis(); - } - - @EventHandler - public void onBallInteract(PlayerInteractAtEntityEvent event) { - if (ball != null && event.getRightClicked().equals(ball)) event.setCancelled(true); - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - // Wenn ein Spieler joinт: Ball-State prüfen und ggf. resynchronisieren. - // Passiert z.B. nach Server-Wechsel via BungeeCord: der Chunk wird neu geladen, - // alte Ball-Entities tauchen wieder auf, die interne Referenz zeigt evtl. auf - // die falsche oder eine ungültige Instanz. - Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { - if (spawnLocation == null || spawnLocation.getWorld() == null) return; - - // Alle alten nexusball-Entities in der Welt suchen - boolean foundValid = false; - for (Entity e : spawnLocation.getWorld().getEntities()) { - if (!(e instanceof ArmorStand as)) continue; - if (!isBallEntity(as)) continue; - - if (ball != null && ball.isValid() && as.getUniqueId().equals(ball.getUniqueId())) { - // Referenz ist noch korrekt und gültig – alles gut - foundValid = true; - } else if (ball == null || !ball.isValid()) { - // Unsere Referenz ist verloren – die gefundene Entity übernehmen - ball = as; - prevPos = as.getLocation().clone(); - lastMoveTime = System.currentTimeMillis(); - foundValid = true; - Bukkit.getLogger().info("[Soccer] Ball-Referenz nach Join wiederhergestellt."); - } else { - // Duplikat – entfernen - as.remove(); - } - } - - if (!foundValid) { - // Kein Ball in der Welt – neu spawnen - respawnBall(); - Bukkit.getLogger().info("[Soccer] Ball nach Join neu gespawnt."); - } - }, 20L); // 1s warten damit der Chunk sicher geladen ist - } - - // ========================================================================= - // COMMANDS - // ========================================================================= - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player p)) { sender.sendMessage("§cNur Spieler."); return true; } - if (!p.hasPermission("nexuslobby.admin")) { p.sendMessage("§cKeine Berechtigung!"); return true; } - if (args.length < 2 || !args[0].equalsIgnoreCase("ball")) return false; - - switch (args[1].toLowerCase()) { - case "setspawn" -> cmdSetSpawn(p); - case "respawn" -> { respawnBall(); p.sendMessage("§8[§6Soccer§8] §eBall neu gespawnt."); } - case "remove" -> cmdRemove(p); - case "goal" -> cmdGoal(p, args); - case "score" -> cmdScore(p, args); - default -> sendHelp(p); - } - return true; - } - - private void cmdSetSpawn(Player p) { - spawnLocation = p.getLocation(); - FileConfiguration c = NexusLobby.getInstance().getConfig(); - c.set("ball.spawn.world", spawnLocation.getWorld().getName()); - c.set("ball.spawn.x", spawnLocation.getX()); - c.set("ball.spawn.y", spawnLocation.getY()); - c.set("ball.spawn.z", spawnLocation.getZ()); - NexusLobby.getInstance().saveConfig(); - respawnBall(); - p.sendMessage("§8[§6Soccer§8] §aBall-Spawn gesetzt und Ball respawnt!"); - } - - private void cmdRemove(Player p) { - if (ball != null) { ball.remove(); ball = null; } - removeAllOldBalls(); - p.sendMessage("§8[§6Soccer§8] §cBall entfernt."); - } - - private void cmdGoal(Player p, String[] args) { - if (args.length < 3) { sendGoalHelp(p); return; } - - switch (args[2].toLowerCase()) { - - case "pos1" -> { - selPos1.put(p.getUniqueId(), p.getLocation().clone()); - p.sendMessage("§8[§6Soccer§8] §aPos1 §7gesetzt bei §e" + fmt(p.getLocation())); - p.sendMessage("§8[§6Soccer§8] §7Zur gegenüberliegenden Ecke und §e/nexuslobby ball goal pos2§7 eingeben."); - } - - case "pos2" -> { - selPos2.put(p.getUniqueId(), p.getLocation().clone()); - p.sendMessage("§8[§6Soccer§8] §aPos2 §7gesetzt bei §e" + fmt(p.getLocation())); - p.sendMessage("§8[§6Soccer§8] §7Speichern mit §e/nexuslobby ball goal save <1|2>§7."); - } - - case "save" -> { - if (args.length < 5) { p.sendMessage("§cVerwendung: /nexuslobby ball goal save <1|2>"); return; } - Location p1 = selPos1.get(p.getUniqueId()); - Location p2 = selPos2.get(p.getUniqueId()); - if (p1 == null || p2 == null) { p.sendMessage("§cZuerst pos1 und pos2 setzen!"); return; } - int team; - try { team = Integer.parseInt(args[4]); } - catch (NumberFormatException e) { p.sendMessage("§cTeam muss 1 oder 2 sein!"); return; } - if (team != 1 && team != 2) { p.sendMessage("§cTeam muss 1 §9(Blau)§c oder 2 §c(Rot) sein!"); return; } - String name = args[3]; - SoccerGoal goal = new SoccerGoal(name, p1, p2, team); - goals.put(name, goal); - saveGoal(goal); - selPos1.remove(p.getUniqueId()); selPos2.remove(p.getUniqueId()); - p.sendMessage("§8[§6Soccer§8] §aTor §e" + name + " §afür " + (team==1?"§9":"§c") + "Team " + team + " §aerstellt!"); - p.sendMessage("§8[§6Soccer§8] §7Nutze §e/nexuslobby ball goal show §7um die Box zu sehen."); - drawGoalFrame(goal); - } - - case "delete" -> { - if (args.length < 4) { p.sendMessage("§cVerwendung: /nexuslobby ball goal delete "); return; } - String name = args[3]; - if (goals.remove(name) != null) { - deleteGoal(name); - p.sendMessage("§8[§6Soccer§8] §cTor §e" + name + " §cgelöscht."); - } else { - p.sendMessage("§cTor '" + name + "' nicht gefunden. Tore: " + String.join(", ", goals.keySet())); - } - } - - case "list" -> { - if (goals.isEmpty()) { p.sendMessage("§8[§6Soccer§8] §7Keine Tore gesetzt."); return; } - p.sendMessage("§8[§6Soccer§8] §6Registrierte Tore §8(" + goals.size() + ")§6:"); - goals.forEach((name, g) -> p.sendMessage( - "§8 » §e" + name + " §8— " + (g.team==1?"§9Blau":"§cRot") + - " §8| §7" + fmt(g.pos1) + " §8→ §7" + fmt(g.pos2))); - } - - case "show" -> { - for (int i = 0; i < 10; i++) { - final int tick = i * 4; - Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), this::drawAllGoalParticles, tick); - } - p.sendMessage("§8[§6Soccer§8] §aTore werden für ~2 Sekunden angezeigt."); - } - - case "debug" -> { - if (ball == null || !ball.isValid()) { p.sendMessage("§8[§6Soccer§8] §cKein Ball."); return; } - Location bl = ball.getLocation(); - Vector bv = ball.getVelocity(); - p.sendMessage("§8[§6Soccer§8] §6Ball getLocation(): §f" + fmtFull(bl)); - p.sendMessage("§8[§6Soccer§8] §6Ball getVelocity(): §f" + fmtVec(bv) + " §7(speed=" + String.format("%.4f", bv.length()) + ")"); - p.sendMessage("§8[§6Soccer§8] §6prevPos: §f" + (prevPos != null ? fmtFull(prevPos) : "null")); - if (!goals.isEmpty()) { - p.sendMessage("§8[§6Soccer§8] §7Direkter Punkt-Check:"); - for (SoccerGoal g : goals.values()) { - boolean h0 = g.contains(bl.clone()); - boolean h1 = g.contains(bl.clone().add(0, 0.45, 0)); - boolean h2 = g.contains(bl.clone().add(0, 0.9, 0)); - p.sendMessage(" §e" + g.name + " §7Fuß=" + cb(h0) + " Mitte=" + cb(h1) + " Kopf=" + cb(h2)); - } - } - } - - default -> sendGoalHelp(p); - } - } - - private void cmdScore(Player p, String[] args) { - if (args.length >= 3 && args[2].equalsIgnoreCase("reset")) { - scores.put(1, 0); - scores.put(2, 0); - for (Player pl : Bukkit.getOnlinePlayers()) { - pl.sendMessage("§8[§6Soccer§8] §eStand wurde zurückgesetzt! §8| §fNeuer Stand: " + getScoreString()); - pl.sendTitle("§eStand zurückgesetzt!", "§90 §8: §c0", 5, 40, 10); - pl.playSound(pl.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1.2f); - } - Bukkit.getLogger().info("[Soccer] Stand von " + p.getName() + " zurückgesetzt."); - } else { - p.sendMessage("§8[§6Soccer§8] §6Aktueller Stand: §f" + getScoreString()); - p.sendMessage("§8[§6Soccer§8] §7Reset: §e/nexuslobby ball score reset"); - } - } - - // ========================================================================= - // HILFSMETHODEN - // ========================================================================= - - private String getScoreString() { - return "§9" + scores.getOrDefault(1, 0) + " §8: §c" + scores.getOrDefault(2, 0); - } - - private static String fmt(Location l) { - return String.format("%.0f/%.0f/%.0f", l.getX(), l.getY(), l.getZ()); - } - private static String fmtFull(Location l) { - return String.format("%.3f/%.3f/%.3f", l.getX(), l.getY(), l.getZ()); - } - private static String fmtVec(Vector v) { - return String.format("%.3f/%.3f/%.3f", v.getX(), v.getY(), v.getZ()); - } - private static String cb(boolean b) { return b ? "§atrue §r" : "§cfalse §r"; } - - private void loadConfigLocation() { - FileConfiguration c = NexusLobby.getInstance().getConfig(); - if (!c.contains("ball.spawn.world")) return; - String wName = c.getString("ball.spawn.world"); - World world = Bukkit.getWorld(wName); - if (world == null) { - Bukkit.getLogger().warning("[Soccer] Spawn-Welt '" + wName + "' nicht gefunden!"); - return; - } - spawnLocation = new Location(world, - c.getDouble("ball.spawn.x"), c.getDouble("ball.spawn.y"), c.getDouble("ball.spawn.z")); - } - - private void sendHelp(Player p) { - p.sendMessage("§8§m─────────────§r §6§lSoccer §r§8§m─────────────"); - p.sendMessage("§e/nexuslobby ball setspawn §7Ball-Spawn setzen"); - p.sendMessage("§e/nexuslobby ball respawn §7Ball neu spawnen"); - p.sendMessage("§e/nexuslobby ball remove §7Ball entfernen"); - p.sendMessage("§e/nexuslobby ball goal ... §7Tore verwalten"); - p.sendMessage("§e/nexuslobby ball score §7Stand anzeigen"); - p.sendMessage("§e/nexuslobby ball score reset §7Stand zurücksetzen"); - p.sendMessage("§8§m─────────────────────────────────────────"); - } - - private void sendGoalHelp(Player p) { - p.sendMessage("§8§m─────────────§r §6§lTore einrichten §r§8§m─────────────"); - p.sendMessage("§71. §7Stehe in eine Ecke des Tors:"); - p.sendMessage(" §e/nexuslobby ball goal pos1"); - p.sendMessage("§72. §7Zur gegenüberliegenden Ecke gehen:"); - p.sendMessage(" §e/nexuslobby ball goal pos2"); - p.sendMessage("§73. §7Speichern §8(Team 1=§9Blau§8, 2=§cRot§8)§7:"); - p.sendMessage(" §e/nexuslobby ball goal save <1|2>"); - p.sendMessage("§e/nexuslobby ball goal delete §7Tor löschen"); - p.sendMessage("§e/nexuslobby ball goal list §7Alle Tore"); - p.sendMessage("§e/nexuslobby ball goal show §7Tore anzeigen"); - p.sendMessage("§e/nexuslobby ball goal debug §7Ball-Position debuggen"); - p.sendMessage("§8§m─────────────────────────────────────────────────"); - } +package de.nexuslobby.modules.ball; + +import de.nexuslobby.NexusLobby; +import de.nexuslobby.api.Module; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.util.Vector; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; + +public class SoccerModule implements Module, Listener, CommandExecutor { + + // ========================================================================= + // KONSTANTEN + // ========================================================================= + private static final String TEXTURE_URL = + "http://textures.minecraft.net/texture/451f8cfcfb85d77945dc6a3618414093e70436b46d2577b28c727f1329b7265e"; + private static final String BALL_TAG = "nexusball_entity"; + private static final String BALL_NAME = "NexusBall"; + + private static final double DRIBBLE_DETECTION_RADIUS = 0.7; + private static final double DRIBBLE_HEIGHT = 0.5; + private static final double DRIBBLE_FORCE = 0.35; + private static final double DRIBBLE_LIFT = 0.12; + private static final double KICK_FORCE = 1.35; + private static final double KICK_LIFT = 0.38; + private static final double WALL_BOUNCE_DAMPING = 0.75; + private static final double WALL_CHECK_DISTANCE = 1.3; + private static final double VOID_THRESHOLD = -5.0; + private static final double CLEANUP_RADIUS = 5.0; + + // Verhindert dass Dribbling einen Kick sofort überschreibt. + private static final long KICK_DRIBBLE_LOCK_MS = 300L; + + private static final double PARTICLE_SPEED_HIGH = 0.85; + private static final double PARTICLE_SPEED_MEDIUM = 0.45; + private static final double PARTICLE_SPEED_MIN = 0.05; + + // Tor-Erkennung: Schrittweite beim Segment-Check in Blöcken. + // 0.1 → bei 1.35 b/t Kick-Speed ~14 Prüfpunkte pro Tick. Sehr sicher. + private static final double CHECK_STEP = 0.1; + + // ── Tor-Partikel ────────────────────────────────────────────────────────── + private static final Particle.DustOptions DUST_TEAM1 = new Particle.DustOptions(Color.fromRGB(30, 120, 255), 1.2f); + private static final Particle.DustOptions DUST_TEAM2 = new Particle.DustOptions(Color.fromRGB(255, 50, 50), 1.2f); + private static final Particle.DustOptions DUST_GOAL_FLASH = new Particle.DustOptions(Color.YELLOW, 2.0f); + private static final double PARTICLE_STEP = 0.35; + private static final long GOAL_RESPAWN_DELAY = 80L; + private static final long GOAL_COOLDOWN_MS = 3_000L; + + // ========================================================================= + // LAUFZEIT-ZUSTAND + // ========================================================================= + private ArmorStand ball; + private Location spawnLocation; + private long lastMoveTime; + private long lastGoalTime = 0L; + private long lastKickTime = 0L; + + // ── Tor-Erkennung: Position aus dem VORHERIGEN Tick ─────────────────────── + // + // WARUM DAS FUNKTIONIERT: + // getLocation() eines ArmorStands liefert in Tick N die Position aus Tick N-1. + // Das klingt wie ein Problem, ist aber tatsächlich die Lösung: + // + // Wenn wir in jedem Tick: + // 1. prevPos = die aktuelle getLocation() merken (= echte Position aus letztem Tick) + // 2. Am ENDE des Ticks: currPos = getLocation() lesen + // 3. Das Segment prevPos→currPos prüfen + // + // Dann deckt dieses Segment exakt den Weg ab den der Ball in Minecraft + // in den letzten 50ms zurückgelegt hat – egal wie schnell, egal ob Kick + // oder Dribble oder geschoben. + // + // WICHTIG: prevPos wird am ANFANG des Ticks gesetzt (bevor Physik-Berechnungen + // die Velocity ändern), currPos am ENDE. So ist das Segment vollständig. + private Location prevPos = null; + + private final Map goals = new LinkedHashMap<>(); + private final Map scores = new HashMap<>(); + private final Map selPos1 = new HashMap<>(); + private final Map selPos2 = new HashMap<>(); + + // ========================================================================= + // MODULE-LIFECYCLE + // ========================================================================= + + @Override public String getName() { return "Soccer"; } + + @Override + public void onEnable() { + Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); + loadConfigLocation(); + loadGoals(); + scores.put(1, 0); + scores.put(2, 0); + + removeAllOldBalls(); + Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { + removeAllOldBalls(); + spawnBall(); + }, 40L); + Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), this::removeAllOldBalls, 100L); + + // ── Physik-Tick ──────────────────────────────────────────────────────── + Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), () -> { + if (ball != null && ball.isValid()) removeDuplicateBalls(); + if (ball == null || !ball.isValid()) return; + + // ── SCHRITT 1: Aktuelle Position als "vorherige" speichern ───────── + // getLocation() ist hier die Position aus dem letzten Server-Tick – + // d.h. die Position BEVOR die Physik dieses Ticks berechnet wurde. + Location currentPos = ball.getLocation().clone(); + + // ── SCHRITT 2: Physik und Spieler-Interaktion verarbeiten ────────── + Vector vel = ball.getVelocity(); + double speed = vel.length(); + handleWallBounce(vel); + handleParticles(speed); + handleDribbling(); + + // ── SCHRITT 3: Nach Physik die neue Position lesen ───────────────── + // Minecraft wendet nach setVelocity noch Drag (0.98 horizontal, Gravity ~0.08 + // + 0.98 vertikal) an. Wir approximieren: newPos = currentPos + vel * 0.98 + // Das ist deutlich genauer als currentPos + vel und vermeidet 1-Tick-Versatz + // bei der Tor-Erkennung nach schnellen Kicks. + Location newPos = currentPos.clone().add(vel.clone().multiply(0.98)); + + // ── SCHRITT 4: Segment prüfen ───────────────────────────────────── + // Das Segment prevPos → newPos deckt den vollständigen Weg ab den + // der Ball seit dem letzten Tor-Check zurückgelegt hat. + if (!goals.isEmpty() + && System.currentTimeMillis() - lastGoalTime >= GOAL_COOLDOWN_MS + && prevPos != null) { + checkSegment(prevPos, newPos, currentPos.getWorld()); + } + + // ── SCHRITT 5: prevPos für nächsten Tick aktualisieren ───────────── + prevPos = newPos.clone(); + + // ── Respawn-Check ────────────────────────────────────────────────── + long respawnDelayMs = NexusLobby.getInstance().getConfig() + .getLong("ball.respawn_delay", 60) * 1_000L; + if (System.currentTimeMillis() - lastMoveTime > respawnDelayMs + || ball.getLocation().getY() < VOID_THRESHOLD) { + respawnBall(); + } + }, 1L, 1L); + + // ── Tor-Partikel ────────────────────────────────────────────────────── + Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), + this::drawAllGoalParticles, 4L, 4L); + } + + @Override + public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); + if (ball != null && ball.isValid()) { ball.remove(); ball = null; } + } + + // ========================================================================= + // TOR-ERKENNUNG + // ========================================================================= + + /** + * Prüft das Segment von {@code from} nach {@code to} auf Tor-Treffer. + * + * Das Segment wird in CHECK_STEP (0.1 Block) Schritte unterteilt. + * An jedem Punkt werden drei Y-Höhen geprüft (Fuß / Mitte / Kopf des Balls). + * + * @param from Position am Anfang des Ticks + * @param to Position am Ende des Ticks (= from + velocity) + * @param world Welt des Balls + */ + private void checkSegment(Location from, Location to, World world) { + if (world == null) return; + + double dx = to.getX() - from.getX(); + double dy = to.getY() - from.getY(); + double dz = to.getZ() - from.getZ(); + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Ball steht fast still → nur aktuellen Punkt prüfen + if (len < 0.001) { + checkPoint(from.getX(), from.getY(), from.getZ(), world); + return; + } + + int steps = Math.max(1, (int) Math.ceil(len / CHECK_STEP)); + double sx = dx / steps; + double sy = dy / steps; + double sz = dz / steps; + + for (int i = 0; i <= steps; i++) { + double x = from.getX() + sx * i; + double y = from.getY() + sy * i; + double z = from.getZ() + sz * i; + if (checkPoint(x, y, z, world)) return; + } + } + + /** + * Prüft einen Punkt in drei Y-Höhen gegen alle Tore. + * @return true wenn ein Tor gewertet wurde (Schleife soll abbrechen) + */ + private boolean checkPoint(double x, double y, double z, World world) { + for (double yOff : new double[]{0.0, 0.45, 0.9}) { + Location check = new Location(world, x, y + yOff, z); + for (SoccerGoal goal : goals.values()) { + if (goal.contains(check)) { + scoreGoal(goal); + return true; + } + } + } + return false; + } + + // ========================================================================= + // TOR WERTEN + // ========================================================================= + + private void scoreGoal(SoccerGoal goal) { + if (System.currentTimeMillis() - lastGoalTime < GOAL_COOLDOWN_MS) return; + lastGoalTime = System.currentTimeMillis(); + scores.merge(goal.team, 1, Integer::sum); + + String teamColor = goal.team == 1 ? "§9" : "§c"; + String teamName = goal.team == 1 ? "Team Blau" : "Team Rot"; + + if (ball != null && ball.isValid()) ball.setVelocity(new Vector(0, 0, 0)); + prevPos = null; // Reset damit kein Doppel-Trigger aus altem Segment + + for (Player p : Bukkit.getOnlinePlayers()) { + p.sendTitle(teamColor + "§lTOR!", teamColor + teamName + " §8| §fStand: " + getScoreString(), 5, 50, 15); + p.sendMessage("§8[§6Soccer§8] " + teamColor + "§lTOR! §r§7für " + teamName + " §8| §fStand: " + getScoreString()); + p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1f); + } + + spawnGoalCelebration(goal); + Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), this::respawnBall, GOAL_RESPAWN_DELAY); + } + + private void spawnGoalCelebration(SoccerGoal goal) { + Location center = goal.getCenter(); + World world = center.getWorld(); + if (world == null) return; + for (int wave = 0; wave < 6; wave++) { + final double offsetY = wave * 0.25; + Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { + Location wLoc = center.clone().add(0, offsetY, 0); + world.spawnParticle(Particle.DUST, wLoc, 50, 1.2, 0.6, 1.2, 0, DUST_GOAL_FLASH); + world.spawnParticle(Particle.FIREWORK, wLoc, 25, 1.0, 0.8, 1.0, 0.25); + world.spawnParticle(Particle.EXPLOSION, center, 2, 0.4, 0.4, 0.4, 0.05); + }, wave * 7L); + } + } + + // ========================================================================= + // PHYSIK + // ========================================================================= + + private void handleDribbling() { + if (ball == null || !ball.isValid()) return; + if (System.currentTimeMillis() - lastKickTime < KICK_DRIBBLE_LOCK_MS) return; + for (Entity nearby : ball.getNearbyEntities(DRIBBLE_DETECTION_RADIUS, DRIBBLE_HEIGHT, DRIBBLE_DETECTION_RADIUS)) { + if (nearby instanceof Player player) { + Vector dir = ball.getLocation().toVector().subtract(player.getLocation().toVector()); + if (dir.lengthSquared() > 0) { + dir.normalize().setY(DRIBBLE_LIFT); + ball.setVelocity(dir.multiply(DRIBBLE_FORCE)); + lastMoveTime = System.currentTimeMillis(); + } + } + } + } + + private void handleWallBounce(Vector vel) { + // Nur prüfen wenn genug horizontale Bewegung vorhanden ist. + // Y-Komponente absichtlich ignoriert – der Bounce gilt nur für Wände (X/Z), + // nicht für Boden/Decke. So verhindert man dass liegende Boden-Blöcke + // fälschlicherweise als Wand erkannt werden und den Ball dauerhaft blockieren. + double hSpeed = vel.getX() * vel.getX() + vel.getZ() * vel.getZ(); + if (hSpeed < 0.001) return; + + Location loc = ball.getLocation(); + // Y + 0.5 anheben damit wir auf Wand-Höhe prüfen, nicht auf Boden-Ebene. + // Ohne den Offset würde loc.getY() (Boden des ArmorStands) den Bodenblock selbst treffen. + Block nx = loc.clone().add(vel.getX() * WALL_CHECK_DISTANCE, 0.5, 0).getBlock(); + Block nz = loc.clone().add(0, 0.5, vel.getZ() * WALL_CHECK_DISTANCE).getBlock(); + boolean bounced = false; + if (nx.getType().isSolid()) { vel.setX(-vel.getX() * WALL_BOUNCE_DAMPING); bounced = true; } + if (nz.getType().isSolid()) { vel.setZ(-vel.getZ() * WALL_BOUNCE_DAMPING); bounced = true; } + if (bounced) { + ball.setVelocity(vel); + ball.getWorld().playSound(loc, Sound.BLOCK_WOOD_BREAK, 0.6f, 1.3f); + } + } + + private void handleParticles(double speed) { + if (speed < PARTICLE_SPEED_MIN) return; + Location loc = ball.getLocation().add(0, 0.2, 0); + World w = loc.getWorld(); + if (w == null) return; + if (speed > PARTICLE_SPEED_HIGH) w.spawnParticle(Particle.SONIC_BOOM, loc, 1, 0, 0, 0, 0); + else if (speed > PARTICLE_SPEED_MEDIUM) w.spawnParticle(Particle.CRIT, loc, 3, 0.1, 0.1, 0.1, 0.08); + else w.spawnParticle(Particle.SMOKE, loc, 1, 0.05, 0, 0.05, 0.02); + } + + // ========================================================================= + // TOR-PARTIKEL ZEICHNEN + // ========================================================================= + + private void drawAllGoalParticles() { + for (SoccerGoal goal : goals.values()) drawGoalFrame(goal); + } + + private void drawGoalFrame(SoccerGoal goal) { + World world = goal.getWorld(); + if (world == null) return; + Particle.DustOptions dust = goal.team == 1 ? DUST_TEAM1 : DUST_TEAM2; + + double x1 = Math.min(goal.pos1.getX(), goal.pos2.getX()), x2 = Math.max(goal.pos1.getX(), goal.pos2.getX()); + double y1 = Math.min(goal.pos1.getY(), goal.pos2.getY()), y2 = Math.max(goal.pos1.getY(), goal.pos2.getY()); + double z1 = Math.min(goal.pos1.getZ(), goal.pos2.getZ()), z2 = Math.max(goal.pos1.getZ(), goal.pos2.getZ()); + + line(world, x1,y1,z1, x2,y1,z1, dust); line(world, x1,y1,z2, x2,y1,z2, dust); + line(world, x1,y1,z1, x1,y1,z2, dust); line(world, x2,y1,z1, x2,y1,z2, dust); + line(world, x1,y2,z1, x2,y2,z1, dust); line(world, x1,y2,z2, x2,y2,z2, dust); + line(world, x1,y2,z1, x1,y2,z2, dust); line(world, x2,y2,z1, x2,y2,z2, dust); + line(world, x1,y1,z1, x1,y2,z1, dust); line(world, x2,y1,z1, x2,y2,z1, dust); + line(world, x1,y1,z2, x1,y2,z2, dust); line(world, x2,y1,z2, x2,y2,z2, dust); + } + + private void line(World world, double ax, double ay, double az, + double bx, double by, double bz, Particle.DustOptions dust) { + double dx = bx-ax, dy = by-ay, dz = bz-az; + double len = Math.sqrt(dx*dx + dy*dy + dz*dz); + if (len < 0.01) return; + int steps = (int) Math.ceil(len / PARTICLE_STEP); + dx /= steps; dy /= steps; dz /= steps; + for (int i = 0; i <= steps; i++) + world.spawnParticle(Particle.DUST, ax+dx*i, ay+dy*i, az+dz*i, 1, 0,0,0,0, dust, true); + } + + // ========================================================================= + // DATEN-KLASSE: SoccerGoal + // ========================================================================= + + private static class SoccerGoal { + final String name; final Location pos1, pos2; final int team; + + SoccerGoal(String name, Location pos1, Location pos2, int team) { + this.name = name; this.pos1 = pos1; this.pos2 = pos2; this.team = team; + } + + boolean contains(Location loc) { + if (loc.getWorld() == null || !loc.getWorld().equals(pos1.getWorld())) return false; + return loc.getX() >= Math.min(pos1.getX(), pos2.getX()) + && loc.getX() <= Math.max(pos1.getX(), pos2.getX()) + && loc.getY() >= Math.min(pos1.getY(), pos2.getY()) + && loc.getY() <= Math.max(pos1.getY(), pos2.getY()) + && loc.getZ() >= Math.min(pos1.getZ(), pos2.getZ()) + && loc.getZ() <= Math.max(pos1.getZ(), pos2.getZ()); + } + + Location getCenter() { + return new Location(pos1.getWorld(), + (pos1.getX()+pos2.getX())/2.0, + (pos1.getY()+pos2.getY())/2.0, + (pos1.getZ()+pos2.getZ())/2.0); + } + + World getWorld() { return pos1.getWorld(); } + } + + // ========================================================================= + // CONFIG: TORE + // ========================================================================= + + private void loadGoals() { + goals.clear(); + FileConfiguration config = NexusLobby.getInstance().getConfig(); + ConfigurationSection section = config.getConfigurationSection("ball.goals"); + if (section == null) return; + for (String name : section.getKeys(false)) { + String path = "ball.goals." + name; + Location pos1 = loadLoc(config, path + ".pos1"); + Location pos2 = loadLoc(config, path + ".pos2"); + if (pos1 == null || pos2 == null) continue; + goals.put(name, new SoccerGoal(name, pos1, pos2, config.getInt(path + ".team", 1))); + } + Bukkit.getLogger().info("[Soccer] " + goals.size() + " Tor(e) geladen."); + } + + private void saveGoal(SoccerGoal g) { + String path = "ball.goals." + g.name; + FileConfiguration c = NexusLobby.getInstance().getConfig(); + saveLoc(c, path + ".pos1", g.pos1); saveLoc(c, path + ".pos2", g.pos2); + c.set(path + ".team", g.team); + NexusLobby.getInstance().saveConfig(); + } + + private void deleteGoal(String name) { + NexusLobby.getInstance().getConfig().set("ball.goals." + name, null); + NexusLobby.getInstance().saveConfig(); + } + + private static void saveLoc(FileConfiguration c, String path, Location loc) { + if (loc == null || loc.getWorld() == null) return; + c.set(path + ".world", loc.getWorld().getName()); + c.set(path + ".x", loc.getX()); c.set(path + ".y", loc.getY()); c.set(path + ".z", loc.getZ()); + } + + private static Location loadLoc(FileConfiguration c, String path) { + String wName = c.getString(path + ".world"); + if (wName == null) return null; + World world = Bukkit.getWorld(wName); + if (world == null) { Bukkit.getLogger().warning("[Soccer] Welt nicht gefunden: " + wName); return null; } + return new Location(world, c.getDouble(path + ".x"), c.getDouble(path + ".y"), c.getDouble(path + ".z")); + } + + // ========================================================================= + // BALL SPAWNEN / RESPAWNEN + // ========================================================================= + + private void spawnBall() { + if (spawnLocation == null || spawnLocation.getWorld() == null) return; + if (ball != null && ball.isValid()) return; + ball = (ArmorStand) spawnLocation.getWorld().spawnEntity(spawnLocation.clone(), EntityType.ARMOR_STAND); + ball.setInvisible(true); ball.setGravity(true); ball.setBasePlate(false); + ball.setSmall(true); ball.setInvulnerable(false); ball.setArms(false); + ball.setCustomNameVisible(false); ball.setCustomName(BALL_NAME); + ball.setPersistent(true); ball.addScoreboardTag(BALL_TAG); + if (ball.getEquipment() != null) ball.getEquipment().setHelmet(getSoccerHead()); + prevPos = spawnLocation.clone(); + lastMoveTime = System.currentTimeMillis(); + } + + public void respawnBall() { + if (ball != null && ball.isValid()) { ball.remove(); ball = null; } + removeAllOldBalls(); + prevPos = null; + spawnBall(); + } + + private ItemStack getSoccerHead() { + ItemStack head = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) head.getItemMeta(); + if (meta == null) return head; + PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID(), BALL_NAME); + try { profile.getTextures().setSkin(new URL(TEXTURE_URL)); } + catch (MalformedURLException e) { Bukkit.getLogger().warning("[NexusLobby] Ungültige Ball-Textur URL!"); } + meta.setOwnerProfile(profile); + head.setItemMeta(meta); + return head; + } + + // ========================================================================= + // CLEANUP + // ========================================================================= + + private void removeDuplicateBalls() { + if (ball == null || !ball.isValid() || ball.getLocation().getWorld() == null) return; + for (Entity e : ball.getLocation().getWorld().getNearbyEntities(ball.getLocation(), 50, 50, 50)) { + if (e instanceof ArmorStand s && isBallEntity(s) && !s.getUniqueId().equals(ball.getUniqueId())) + s.remove(); + } + } + + private void removeAllOldBalls() { + int removed = 0; + if (spawnLocation != null && spawnLocation.getWorld() != null) + removed = cleanupWorld(spawnLocation.getWorld()); + else + for (World w : Bukkit.getWorlds()) removed += cleanupWorld(w); + if (removed > 0) Bukkit.getLogger().info("[NexusLobby] " + removed + " alte Ball-Entities entfernt."); + } + + private int cleanupWorld(World world) { + int n = 0; + for (Entity e : world.getEntities()) { + if (e instanceof ArmorStand s && isBallEntity(s) + && (ball == null || !s.getUniqueId().equals(ball.getUniqueId()))) { + s.remove(); n++; + } + } + return n; + } + + private boolean isBallEntity(ArmorStand s) { + if (s.getScoreboardTags().contains(BALL_TAG)) return true; + if (BALL_NAME.equals(s.getCustomName())) return true; + if (s.getEquipment() != null && s.getEquipment().getHelmet() != null) { + ItemStack h = s.getEquipment().getHelmet(); + if (h.getType() == Material.PLAYER_HEAD && h.hasItemMeta()) { + SkullMeta m = (SkullMeta) h.getItemMeta(); + if (m.hasOwner() && m.getOwnerProfile() != null + && BALL_NAME.equals(m.getOwnerProfile().getName())) return true; + } + } + if (spawnLocation != null && spawnLocation.getWorld() != null + && s.getWorld().equals(spawnLocation.getWorld()) + && s.isSmall() && s.isInvisible() && !s.hasBasePlate() + && s.getLocation().distance(spawnLocation) < CLEANUP_RADIUS + && s.getEquipment() != null && s.getEquipment().getHelmet() != null + && s.getEquipment().getHelmet().getType() == Material.PLAYER_HEAD) + return true; + return false; + } + + // ========================================================================= + // EVENTS + // ========================================================================= + + @EventHandler + public void onBallPunch(EntityDamageByEntityEvent event) { + if (ball == null || !event.getEntity().equals(ball)) return; + event.setCancelled(true); + if (!(event.getDamager() instanceof Player p)) return; + + Vector dir = p.getLocation().getDirection().normalize(); + dir.multiply(KICK_FORCE).setY(KICK_LIFT); + + // prevPos auf aktuelle Position setzen bevor der Ball sich bewegt – + // so startet das nächste Segment am richtigen Punkt. + prevPos = ball.getLocation().clone(); + lastKickTime = System.currentTimeMillis(); + ball.setVelocity(dir); + + ball.getWorld().playSound(ball.getLocation(), Sound.ENTITY_ZOMBIE_ATTACK_IRON_DOOR, 0.6f, 1.5f); + lastMoveTime = System.currentTimeMillis(); + } + + @EventHandler + public void onBallInteract(PlayerInteractAtEntityEvent event) { + if (ball != null && event.getRightClicked().equals(ball)) event.setCancelled(true); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + // Wenn ein Spieler joinт: Ball-State prüfen und ggf. resynchronisieren. + // Passiert z.B. nach Server-Wechsel via BungeeCord: der Chunk wird neu geladen, + // alte Ball-Entities tauchen wieder auf, die interne Referenz zeigt evtl. auf + // die falsche oder eine ungültige Instanz. + Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { + if (spawnLocation == null || spawnLocation.getWorld() == null) return; + + // Alle alten nexusball-Entities in der Welt suchen + boolean foundValid = false; + for (Entity e : spawnLocation.getWorld().getEntities()) { + if (!(e instanceof ArmorStand as)) continue; + if (!isBallEntity(as)) continue; + + if (ball != null && ball.isValid() && as.getUniqueId().equals(ball.getUniqueId())) { + // Referenz ist noch korrekt und gültig – alles gut + foundValid = true; + } else if (ball == null || !ball.isValid()) { + // Unsere Referenz ist verloren – die gefundene Entity übernehmen + ball = as; + prevPos = as.getLocation().clone(); + lastMoveTime = System.currentTimeMillis(); + foundValid = true; + Bukkit.getLogger().info("[Soccer] Ball-Referenz nach Join wiederhergestellt."); + } else { + // Duplikat – entfernen + as.remove(); + } + } + + if (!foundValid) { + // Kein Ball in der Welt – neu spawnen + respawnBall(); + Bukkit.getLogger().info("[Soccer] Ball nach Join neu gespawnt."); + } + }, 20L); // 1s warten damit der Chunk sicher geladen ist + } + + // ========================================================================= + // COMMANDS + // ========================================================================= + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player p)) { sender.sendMessage("§cNur Spieler."); return true; } + if (!p.hasPermission("nexuslobby.admin")) { p.sendMessage("§cKeine Berechtigung!"); return true; } + if (args.length < 2 || !args[0].equalsIgnoreCase("ball")) return false; + + switch (args[1].toLowerCase()) { + case "setspawn" -> cmdSetSpawn(p); + case "respawn" -> { respawnBall(); p.sendMessage("§8[§6Soccer§8] §eBall neu gespawnt."); } + case "remove" -> cmdRemove(p); + case "goal" -> cmdGoal(p, args); + case "score" -> cmdScore(p, args); + default -> sendHelp(p); + } + return true; + } + + private void cmdSetSpawn(Player p) { + spawnLocation = p.getLocation(); + FileConfiguration c = NexusLobby.getInstance().getConfig(); + c.set("ball.spawn.world", spawnLocation.getWorld().getName()); + c.set("ball.spawn.x", spawnLocation.getX()); + c.set("ball.spawn.y", spawnLocation.getY()); + c.set("ball.spawn.z", spawnLocation.getZ()); + NexusLobby.getInstance().saveConfig(); + respawnBall(); + p.sendMessage("§8[§6Soccer§8] §aBall-Spawn gesetzt und Ball respawnt!"); + } + + private void cmdRemove(Player p) { + if (ball != null) { ball.remove(); ball = null; } + removeAllOldBalls(); + p.sendMessage("§8[§6Soccer§8] §cBall entfernt."); + } + + private void cmdGoal(Player p, String[] args) { + if (args.length < 3) { sendGoalHelp(p); return; } + + switch (args[2].toLowerCase()) { + + case "pos1" -> { + selPos1.put(p.getUniqueId(), p.getLocation().clone()); + p.sendMessage("§8[§6Soccer§8] §aPos1 §7gesetzt bei §e" + fmt(p.getLocation())); + p.sendMessage("§8[§6Soccer§8] §7Zur gegenüberliegenden Ecke und §e/nexuslobby ball goal pos2§7 eingeben."); + } + + case "pos2" -> { + selPos2.put(p.getUniqueId(), p.getLocation().clone()); + p.sendMessage("§8[§6Soccer§8] §aPos2 §7gesetzt bei §e" + fmt(p.getLocation())); + p.sendMessage("§8[§6Soccer§8] §7Speichern mit §e/nexuslobby ball goal save <1|2>§7."); + } + + case "save" -> { + if (args.length < 5) { p.sendMessage("§cVerwendung: /nexuslobby ball goal save <1|2>"); return; } + Location p1 = selPos1.get(p.getUniqueId()); + Location p2 = selPos2.get(p.getUniqueId()); + if (p1 == null || p2 == null) { p.sendMessage("§cZuerst pos1 und pos2 setzen!"); return; } + int team; + try { team = Integer.parseInt(args[4]); } + catch (NumberFormatException e) { p.sendMessage("§cTeam muss 1 oder 2 sein!"); return; } + if (team != 1 && team != 2) { p.sendMessage("§cTeam muss 1 §9(Blau)§c oder 2 §c(Rot) sein!"); return; } + String name = args[3]; + SoccerGoal goal = new SoccerGoal(name, p1, p2, team); + goals.put(name, goal); + saveGoal(goal); + selPos1.remove(p.getUniqueId()); selPos2.remove(p.getUniqueId()); + p.sendMessage("§8[§6Soccer§8] §aTor §e" + name + " §afür " + (team==1?"§9":"§c") + "Team " + team + " §aerstellt!"); + p.sendMessage("§8[§6Soccer§8] §7Nutze §e/nexuslobby ball goal show §7um die Box zu sehen."); + drawGoalFrame(goal); + } + + case "delete" -> { + if (args.length < 4) { p.sendMessage("§cVerwendung: /nexuslobby ball goal delete "); return; } + String name = args[3]; + if (goals.remove(name) != null) { + deleteGoal(name); + p.sendMessage("§8[§6Soccer§8] §cTor §e" + name + " §cgelöscht."); + } else { + p.sendMessage("§cTor '" + name + "' nicht gefunden. Tore: " + String.join(", ", goals.keySet())); + } + } + + case "list" -> { + if (goals.isEmpty()) { p.sendMessage("§8[§6Soccer§8] §7Keine Tore gesetzt."); return; } + p.sendMessage("§8[§6Soccer§8] §6Registrierte Tore §8(" + goals.size() + ")§6:"); + goals.forEach((name, g) -> p.sendMessage( + "§8 » §e" + name + " §8— " + (g.team==1?"§9Blau":"§cRot") + + " §8| §7" + fmt(g.pos1) + " §8→ §7" + fmt(g.pos2))); + } + + case "show" -> { + for (int i = 0; i < 10; i++) { + final int tick = i * 4; + Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), this::drawAllGoalParticles, tick); + } + p.sendMessage("§8[§6Soccer§8] §aTore werden für ~2 Sekunden angezeigt."); + } + + case "debug" -> { + if (ball == null || !ball.isValid()) { p.sendMessage("§8[§6Soccer§8] §cKein Ball."); return; } + Location bl = ball.getLocation(); + Vector bv = ball.getVelocity(); + p.sendMessage("§8[§6Soccer§8] §6Ball getLocation(): §f" + fmtFull(bl)); + p.sendMessage("§8[§6Soccer§8] §6Ball getVelocity(): §f" + fmtVec(bv) + " §7(speed=" + String.format("%.4f", bv.length()) + ")"); + p.sendMessage("§8[§6Soccer§8] §6prevPos: §f" + (prevPos != null ? fmtFull(prevPos) : "null")); + if (!goals.isEmpty()) { + p.sendMessage("§8[§6Soccer§8] §7Direkter Punkt-Check:"); + for (SoccerGoal g : goals.values()) { + boolean h0 = g.contains(bl.clone()); + boolean h1 = g.contains(bl.clone().add(0, 0.45, 0)); + boolean h2 = g.contains(bl.clone().add(0, 0.9, 0)); + p.sendMessage(" §e" + g.name + " §7Fuß=" + cb(h0) + " Mitte=" + cb(h1) + " Kopf=" + cb(h2)); + } + } + } + + default -> sendGoalHelp(p); + } + } + + private void cmdScore(Player p, String[] args) { + if (args.length >= 3 && args[2].equalsIgnoreCase("reset")) { + scores.put(1, 0); + scores.put(2, 0); + for (Player pl : Bukkit.getOnlinePlayers()) { + pl.sendMessage("§8[§6Soccer§8] §eStand wurde zurückgesetzt! §8| §fNeuer Stand: " + getScoreString()); + pl.sendTitle("§eStand zurückgesetzt!", "§90 §8: §c0", 5, 40, 10); + pl.playSound(pl.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1.2f); + } + Bukkit.getLogger().info("[Soccer] Stand von " + p.getName() + " zurückgesetzt."); + } else { + p.sendMessage("§8[§6Soccer§8] §6Aktueller Stand: §f" + getScoreString()); + p.sendMessage("§8[§6Soccer§8] §7Reset: §e/nexuslobby ball score reset"); + } + } + + // ========================================================================= + // HILFSMETHODEN + // ========================================================================= + + private String getScoreString() { + return "§9" + scores.getOrDefault(1, 0) + " §8: §c" + scores.getOrDefault(2, 0); + } + + private static String fmt(Location l) { + return String.format("%.0f/%.0f/%.0f", l.getX(), l.getY(), l.getZ()); + } + private static String fmtFull(Location l) { + return String.format("%.3f/%.3f/%.3f", l.getX(), l.getY(), l.getZ()); + } + private static String fmtVec(Vector v) { + return String.format("%.3f/%.3f/%.3f", v.getX(), v.getY(), v.getZ()); + } + private static String cb(boolean b) { return b ? "§atrue §r" : "§cfalse §r"; } + + private void loadConfigLocation() { + FileConfiguration c = NexusLobby.getInstance().getConfig(); + if (!c.contains("ball.spawn.world")) return; + String wName = c.getString("ball.spawn.world"); + World world = Bukkit.getWorld(wName); + if (world == null) { + Bukkit.getLogger().warning("[Soccer] Spawn-Welt '" + wName + "' nicht gefunden!"); + return; + } + spawnLocation = new Location(world, + c.getDouble("ball.spawn.x"), c.getDouble("ball.spawn.y"), c.getDouble("ball.spawn.z")); + } + + private void sendHelp(Player p) { + p.sendMessage("§8§m─────────────§r §6§lSoccer §r§8§m─────────────"); + p.sendMessage("§e/nexuslobby ball setspawn §7Ball-Spawn setzen"); + p.sendMessage("§e/nexuslobby ball respawn §7Ball neu spawnen"); + p.sendMessage("§e/nexuslobby ball remove §7Ball entfernen"); + p.sendMessage("§e/nexuslobby ball goal ... §7Tore verwalten"); + p.sendMessage("§e/nexuslobby ball score §7Stand anzeigen"); + p.sendMessage("§e/nexuslobby ball score reset §7Stand zurücksetzen"); + p.sendMessage("§8§m─────────────────────────────────────────"); + } + + private void sendGoalHelp(Player p) { + p.sendMessage("§8§m─────────────§r §6§lTore einrichten §r§8§m─────────────"); + p.sendMessage("§71. §7Stehe in eine Ecke des Tors:"); + p.sendMessage(" §e/nexuslobby ball goal pos1"); + p.sendMessage("§72. §7Zur gegenüberliegenden Ecke gehen:"); + p.sendMessage(" §e/nexuslobby ball goal pos2"); + p.sendMessage("§73. §7Speichern §8(Team 1=§9Blau§8, 2=§cRot§8)§7:"); + p.sendMessage(" §e/nexuslobby ball goal save <1|2>"); + p.sendMessage("§e/nexuslobby ball goal delete §7Tor löschen"); + p.sendMessage("§e/nexuslobby ball goal list §7Alle Tore"); + p.sendMessage("§e/nexuslobby ball goal show §7Tore anzeigen"); + p.sendMessage("§e/nexuslobby ball goal debug §7Ball-Position debuggen"); + p.sendMessage("§8§m─────────────────────────────────────────────────"); + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/border/BorderCommand.java b/src/main/java/de/nexuslobby/modules/border/BorderCommand.java index 5685f46..b127611 100644 --- a/src/main/java/de/nexuslobby/modules/border/BorderCommand.java +++ b/src/main/java/de/nexuslobby/modules/border/BorderCommand.java @@ -1,108 +1,108 @@ -package de.nexuslobby.commands; - -import de.nexuslobby.NexusLobby; -import de.nexuslobby.modules.portal.PortalCommand; -import de.nexuslobby.modules.border.BorderModule; -import org.bukkit.Location; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -public class BorderCommand implements CommandExecutor { - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player p)) return true; - if (!p.hasPermission("nexuslobby.admin")) { - p.sendMessage("§8[§6Nexus§8] §cKeine Rechte."); - return true; - } - - if (args.length == 0) { - p.sendMessage("§8§m------------------------------------"); - p.sendMessage("§6§lNexus WorldBorder Setup"); - p.sendMessage("§e/border circle §7- Kreis um dich herum"); - p.sendMessage("§e/border square §7- Nutzt Axt-Markierung (Viereck)"); - p.sendMessage("§e/border disable §7- Grenze deaktivieren"); - p.sendMessage("§8§m------------------------------------"); - return true; - } - - var config = NexusLobby.getInstance().getConfig(); - - switch (args[0].toLowerCase()) { - case "circle" -> { - if (args.length < 2) { - p.sendMessage("§cBitte gib einen Radius an."); - return true; - } - try { - double radius = Double.parseDouble(args[1]); - Location loc = p.getLocation(); - - config.set("worldborder.type", "CIRCLE"); - config.set("worldborder.enabled", true); - config.set("worldborder.radius", radius); - - // FIX: Location als einzelne Werte speichern statt als Objekt. - // config.set(key, Location) ist nach Neustarts unzuverlässig, - // weil config.getLocation() die Welt zum Deserialisierungs- - // zeitpunkt auflösen muss und das fehlschlagen kann. - config.set("worldborder.center.world", loc.getWorld().getName()); - config.set("worldborder.center.x", loc.getX()); - config.set("worldborder.center.y", loc.getY()); - config.set("worldborder.center.z", loc.getZ()); - - p.sendMessage("§8[§6Nexus§8] §aKreis-Grenze (Radius: " + radius + ") gesetzt."); - } catch (NumberFormatException e) { - p.sendMessage("§cUngültige Zahl."); - return true; - } - } - case "square" -> { - Location l1 = PortalCommand.getSelection1(p); - Location l2 = PortalCommand.getSelection2(p); - if (l1 == null || l2 == null) { - p.sendMessage("§8[§6Nexus§8] §cBitte markiere erst 2 Punkte mit der Portalwand!"); - return true; - } - - config.set("worldborder.type", "SQUARE"); - config.set("worldborder.enabled", true); - - // FIX: Beide Positionen als einzelne Werte speichern - config.set("worldborder.pos1.world", l1.getWorld().getName()); - config.set("worldborder.pos1.x", l1.getX()); - config.set("worldborder.pos1.y", l1.getY()); - config.set("worldborder.pos1.z", l1.getZ()); - - config.set("worldborder.pos2.world", l2.getWorld().getName()); - config.set("worldborder.pos2.x", l2.getX()); - config.set("worldborder.pos2.y", l2.getY()); - config.set("worldborder.pos2.z", l2.getZ()); - - p.sendMessage("§8[§6Nexus§8] §aViereckige Grenze erfolgreich gesetzt."); - } - case "disable" -> { - config.set("worldborder.enabled", false); - p.sendMessage("§8[§6Nexus§8] §cGrenze wurde deaktiviert."); - } - default -> { - p.sendMessage("§cUnbekannter Unterbefehl."); - return true; - } - } - - NexusLobby.getInstance().saveConfig(); - - // Update das Modul direkt im RAM - BorderModule module = NexusLobby.getInstance().getModuleManager().getModule(BorderModule.class); - if (module != null) { - module.reloadConfig(); - } - - return true; - } +package de.nexuslobby.commands; + +import de.nexuslobby.NexusLobby; +import de.nexuslobby.modules.portal.PortalCommand; +import de.nexuslobby.modules.border.BorderModule; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class BorderCommand implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player p)) return true; + if (!p.hasPermission("nexuslobby.admin")) { + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cKeine Rechte."); + return true; + } + + if (args.length == 0) { + p.sendMessage("§8§m------------------------------------"); + p.sendMessage("§6§lNexus WorldBorder Setup"); + p.sendMessage("§e/border circle §7- Kreis um dich herum"); + p.sendMessage("§e/border square §7- Nutzt Axt-Markierung (Viereck)"); + p.sendMessage("§e/border disable §7- Grenze deaktivieren"); + p.sendMessage("§8§m------------------------------------"); + return true; + } + + var config = NexusLobby.getInstance().getConfig(); + + switch (args[0].toLowerCase()) { + case "circle" -> { + if (args.length < 2) { + p.sendMessage("§cBitte gib einen Radius an."); + return true; + } + try { + double radius = Double.parseDouble(args[1]); + Location loc = p.getLocation(); + + config.set("worldborder.type", "CIRCLE"); + config.set("worldborder.enabled", true); + config.set("worldborder.radius", radius); + + // FIX: Location als einzelne Werte speichern statt als Objekt. + // config.set(key, Location) ist nach Neustarts unzuverlässig, + // weil config.getLocation() die Welt zum Deserialisierungs- + // zeitpunkt auflösen muss und das fehlschlagen kann. + config.set("worldborder.center.world", loc.getWorld().getName()); + config.set("worldborder.center.x", loc.getX()); + config.set("worldborder.center.y", loc.getY()); + config.set("worldborder.center.z", loc.getZ()); + + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aKreis-Grenze (Radius: " + radius + ") gesetzt."); + } catch (NumberFormatException e) { + p.sendMessage("§cUngültige Zahl."); + return true; + } + } + case "square" -> { + Location l1 = PortalCommand.getSelection1(p); + Location l2 = PortalCommand.getSelection2(p); + if (l1 == null || l2 == null) { + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cBitte markiere erst 2 Punkte mit der Portalwand!"); + return true; + } + + config.set("worldborder.type", "SQUARE"); + config.set("worldborder.enabled", true); + + // FIX: Beide Positionen als einzelne Werte speichern + config.set("worldborder.pos1.world", l1.getWorld().getName()); + config.set("worldborder.pos1.x", l1.getX()); + config.set("worldborder.pos1.y", l1.getY()); + config.set("worldborder.pos1.z", l1.getZ()); + + config.set("worldborder.pos2.world", l2.getWorld().getName()); + config.set("worldborder.pos2.x", l2.getX()); + config.set("worldborder.pos2.y", l2.getY()); + config.set("worldborder.pos2.z", l2.getZ()); + + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aViereckige Grenze erfolgreich gesetzt."); + } + case "disable" -> { + config.set("worldborder.enabled", false); + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cGrenze wurde deaktiviert."); + } + default -> { + p.sendMessage("§cUnbekannter Unterbefehl."); + return true; + } + } + + NexusLobby.getInstance().saveConfig(); + + // Update das Modul direkt im RAM + BorderModule module = NexusLobby.getInstance().getModuleManager().getModule(BorderModule.class); + if (module != null) { + module.reloadConfig(); + } + + return true; + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/border/BorderModule.java b/src/main/java/de/nexuslobby/modules/border/BorderModule.java index 8f5e7fd..c3b8944 100644 --- a/src/main/java/de/nexuslobby/modules/border/BorderModule.java +++ b/src/main/java/de/nexuslobby/modules/border/BorderModule.java @@ -1,162 +1,162 @@ -package de.nexuslobby.modules.border; - -import de.nexuslobby.NexusLobby; -import de.nexuslobby.api.Module; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Sound; -import org.bukkit.World; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.HandlerList; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerMoveEvent; - -/** - * Verwaltet die Lobby-Begrenzung (Kreis oder Rechteck). - * Speichert Daten in der Haupt-config.yml unter 'worldborder'. - * - * FIX: Locations werden als einzelne Werte (worldName, x, y, z) gespeichert - * statt als serialisiertes Location-Objekt, da config.getLocation() - * nach einem Neustart unzuverlässig ist. - */ -public class BorderModule implements Module, Listener { - - private String type; - // FIX: Statt Location-Objekte die rohen Werte cachen - private String pos1World, pos2World, centerWorld; - private double pos1X, pos1Y, pos1Z; - private double pos2X, pos2Y, pos2Z; - private double centerX, centerZ; - private double radius; - private boolean enabled; - private boolean hasPos1, hasPos2, hasCenter; - - @Override - public String getName() { return "WorldBorder"; } - - @Override - public void onEnable() { - reloadConfig(); - Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); - } - - @Override - public void onDisable() { - // FIX: Listener beim Deaktivieren abmelden, damit nach /reload - // keine doppelten Events gefeuert werden. - HandlerList.unregisterAll(this); - } - - /** - * Lädt die Border-Einstellungen aus der config.yml. - * Wird auch von BorderCommand und NexusLobby.reloadPlugin() aufgerufen. - * - * FIX: Robuste Methode - liest world-Name und einzelne Koordinaten, - * kein config.getLocation() mehr (das nach Neustarts versagen kann). - */ - public void reloadConfig() { - FileConfiguration config = NexusLobby.getInstance().getConfig(); - - this.enabled = config.getBoolean("worldborder.enabled", false); - this.type = config.getString("worldborder.type", "CIRCLE").toUpperCase(); - this.radius = config.getDouble("worldborder.radius", 50.0); - - // --- CIRCLE-Daten laden --- - this.hasCenter = config.contains("worldborder.center.world"); - if (hasCenter) { - this.centerWorld = config.getString("worldborder.center.world"); - this.centerX = config.getDouble("worldborder.center.x"); - this.centerZ = config.getDouble("worldborder.center.z"); - } - - // --- SQUARE-Daten laden --- - this.hasPos1 = config.contains("worldborder.pos1.world"); - this.hasPos2 = config.contains("worldborder.pos2.world"); - if (hasPos1) { - this.pos1World = config.getString("worldborder.pos1.world"); - this.pos1X = config.getDouble("worldborder.pos1.x"); - this.pos1Y = config.getDouble("worldborder.pos1.y"); - this.pos1Z = config.getDouble("worldborder.pos1.z"); - } - if (hasPos2) { - this.pos2World = config.getString("worldborder.pos2.world"); - this.pos2X = config.getDouble("worldborder.pos2.x"); - this.pos2Y = config.getDouble("worldborder.pos2.y"); - this.pos2Z = config.getDouble("worldborder.pos2.z"); - } - } - - @EventHandler - public void onMove(PlayerMoveEvent event) { - if (!enabled || event.getTo() == null) return; - - // Performance: Nur prüfen wenn sich Block-Koordinaten ändern - if (event.getFrom().getBlockX() == event.getTo().getBlockX() && - event.getFrom().getBlockZ() == event.getTo().getBlockZ() && - event.getFrom().getBlockY() == event.getTo().getBlockY()) return; - - Player player = event.getPlayer(); - - // FIX: nexuslobby.border.bypass statt nur nexuslobby.admin prüfen - if (player.hasPermission("nexuslobby.border.bypass") || - player.hasPermission("nexuslobby.admin") || - player.getGameMode().name().equals("SPECTATOR")) return; - - Location to = event.getTo(); - boolean outside = false; - - // --- Prüfung: Kreis-Border --- - if (type.equals("CIRCLE") && hasCenter) { - World cWorld = Bukkit.getWorld(centerWorld); - if (cWorld != null && to.getWorld().equals(cWorld)) { - double distSq = Math.pow(to.getX() - centerX, 2) + Math.pow(to.getZ() - centerZ, 2); - if (distSq > radius * radius) outside = true; - } - } - // --- Prüfung: Rechteck-Border (Square) --- - else if (type.equals("SQUARE") && hasPos1 && hasPos2) { - World bWorld = Bukkit.getWorld(pos1World); - if (bWorld != null && to.getWorld().equals(bWorld)) { - double minX = Math.min(pos1X, pos2X); - double maxX = Math.max(pos1X, pos2X); - double minZ = Math.min(pos1Z, pos2Z); - double maxZ = Math.max(pos1Z, pos2Z); - if (to.getX() < minX || to.getX() > maxX || - to.getZ() < minZ || to.getZ() > maxZ) { - outside = true; - } - } - } - - if (outside) { - Location spawnLocation = getMainSpawnLocation(); - if (spawnLocation != null) { - player.teleport(spawnLocation); - player.playSound(player.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 0.5f); - player.sendMessage("§8[§6Nexus§8] §cDu hast den Lobby-Bereich verlassen!"); - } - } - } - - /** - * Holt den Spawnpunkt aus der Config (Pfad: spawn.world, spawn.x, etc.) - */ - private Location getMainSpawnLocation() { - FileConfiguration config = NexusLobby.getInstance().getConfig(); - String worldName = config.getString("spawn.world"); - if (worldName != null) { - World w = Bukkit.getWorld(worldName); - if (w != null) { - return new Location(w, - config.getDouble("spawn.x"), - config.getDouble("spawn.y"), - config.getDouble("spawn.z"), - (float) config.getDouble("spawn.yaw"), - (float) config.getDouble("spawn.pitch")); - } - } - return Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0).getSpawnLocation(); - } +package de.nexuslobby.modules.border; + +import de.nexuslobby.NexusLobby; +import de.nexuslobby.api.Module; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerMoveEvent; + +/** + * Verwaltet die Lobby-Begrenzung (Kreis oder Rechteck). + * Speichert Daten in der Haupt-config.yml unter 'worldborder'. + * + * FIX: Locations werden als einzelne Werte (worldName, x, y, z) gespeichert + * statt als serialisiertes Location-Objekt, da config.getLocation() + * nach einem Neustart unzuverlässig ist. + */ +public class BorderModule implements Module, Listener { + + private String type; + // FIX: Statt Location-Objekte die rohen Werte cachen + private String pos1World, pos2World, centerWorld; + private double pos1X, pos1Y, pos1Z; + private double pos2X, pos2Y, pos2Z; + private double centerX, centerZ; + private double radius; + private boolean enabled; + private boolean hasPos1, hasPos2, hasCenter; + + @Override + public String getName() { return "WorldBorder"; } + + @Override + public void onEnable() { + reloadConfig(); + Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); + } + + @Override + public void onDisable() { + // FIX: Listener beim Deaktivieren abmelden, damit nach /reload + // keine doppelten Events gefeuert werden. + HandlerList.unregisterAll(this); + } + + /** + * Lädt die Border-Einstellungen aus der config.yml. + * Wird auch von BorderCommand und NexusLobby.reloadPlugin() aufgerufen. + * + * FIX: Robuste Methode - liest world-Name und einzelne Koordinaten, + * kein config.getLocation() mehr (das nach Neustarts versagen kann). + */ + public void reloadConfig() { + FileConfiguration config = NexusLobby.getInstance().getConfig(); + + this.enabled = config.getBoolean("worldborder.enabled", false); + this.type = config.getString("worldborder.type", "CIRCLE").toUpperCase(); + this.radius = config.getDouble("worldborder.radius", 50.0); + + // --- CIRCLE-Daten laden --- + this.hasCenter = config.contains("worldborder.center.world"); + if (hasCenter) { + this.centerWorld = config.getString("worldborder.center.world"); + this.centerX = config.getDouble("worldborder.center.x"); + this.centerZ = config.getDouble("worldborder.center.z"); + } + + // --- SQUARE-Daten laden --- + this.hasPos1 = config.contains("worldborder.pos1.world"); + this.hasPos2 = config.contains("worldborder.pos2.world"); + if (hasPos1) { + this.pos1World = config.getString("worldborder.pos1.world"); + this.pos1X = config.getDouble("worldborder.pos1.x"); + this.pos1Y = config.getDouble("worldborder.pos1.y"); + this.pos1Z = config.getDouble("worldborder.pos1.z"); + } + if (hasPos2) { + this.pos2World = config.getString("worldborder.pos2.world"); + this.pos2X = config.getDouble("worldborder.pos2.x"); + this.pos2Y = config.getDouble("worldborder.pos2.y"); + this.pos2Z = config.getDouble("worldborder.pos2.z"); + } + } + + @EventHandler + public void onMove(PlayerMoveEvent event) { + if (!enabled || event.getTo() == null) return; + + // Performance: Nur prüfen wenn sich Block-Koordinaten ändern + if (event.getFrom().getBlockX() == event.getTo().getBlockX() && + event.getFrom().getBlockZ() == event.getTo().getBlockZ() && + event.getFrom().getBlockY() == event.getTo().getBlockY()) return; + + Player player = event.getPlayer(); + + // FIX: nexuslobby.border.bypass statt nur nexuslobby.admin prüfen + if (player.hasPermission("nexuslobby.border.bypass") || + player.hasPermission("nexuslobby.admin") || + player.getGameMode().name().equals("SPECTATOR")) return; + + Location to = event.getTo(); + boolean outside = false; + + // --- Prüfung: Kreis-Border --- + if (type.equals("CIRCLE") && hasCenter) { + World cWorld = Bukkit.getWorld(centerWorld); + if (cWorld != null && to.getWorld().equals(cWorld)) { + double distSq = Math.pow(to.getX() - centerX, 2) + Math.pow(to.getZ() - centerZ, 2); + if (distSq > radius * radius) outside = true; + } + } + // --- Prüfung: Rechteck-Border (Square) --- + else if (type.equals("SQUARE") && hasPos1 && hasPos2) { + World bWorld = Bukkit.getWorld(pos1World); + if (bWorld != null && to.getWorld().equals(bWorld)) { + double minX = Math.min(pos1X, pos2X); + double maxX = Math.max(pos1X, pos2X); + double minZ = Math.min(pos1Z, pos2Z); + double maxZ = Math.max(pos1Z, pos2Z); + if (to.getX() < minX || to.getX() > maxX || + to.getZ() < minZ || to.getZ() > maxZ) { + outside = true; + } + } + } + + if (outside) { + Location spawnLocation = getMainSpawnLocation(); + if (spawnLocation != null) { + player.teleport(spawnLocation); + player.playSound(player.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 0.5f); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cDu hast den Lobby-Bereich verlassen!"); + } + } + } + + /** + * Holt den Spawnpunkt aus der Config (Pfad: spawn.world, spawn.x, etc.) + */ + private Location getMainSpawnLocation() { + FileConfiguration config = NexusLobby.getInstance().getConfig(); + String worldName = config.getString("spawn.world"); + if (worldName != null) { + World w = Bukkit.getWorld(worldName); + if (w != null) { + return new Location(w, + config.getDouble("spawn.x"), + config.getDouble("spawn.y"), + config.getDouble("spawn.z"), + (float) config.getDouble("spawn.yaw"), + (float) config.getDouble("spawn.pitch")); + } + } + return Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0).getSpawnLocation(); + } } \ 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 index 1e4e00e..46aabe2 100644 --- a/src/main/java/de/nexuslobby/modules/gadgets/FreezeRay.java +++ b/src/main/java/de/nexuslobby/modules/gadgets/FreezeRay.java @@ -37,7 +37,7 @@ public class FreezeRay { if (target != shooter) { if (GadgetShield.isProtected(target)) { String reason = GadgetShield.isAdminShielded(target) ? "Dieser Spieler hat Gadget-Schutz aktiviert." : "Dieser Spieler ist gerade im Parkour."; - shooter.sendMessage("§8[§6Nexus§8] §7" + reason); + shooter.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§7" + reason); return; } @@ -58,7 +58,7 @@ public class FreezeRay { if (!frozenPlayers.contains(target.getUniqueId())) { frozenPlayers.add(target.getUniqueId()); final Location freezeLocation = target.getLocation(); - target.sendMessage("§8[§6Nexus§8] §bDu wurdest eingefroren!"); + target.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§bDu wurdest eingefroren!"); target.getWorld().playSound(target.getLocation(), Sound.BLOCK_GLASS_BREAK, 1.0F, 0.5F); (new BukkitRunnable() { int ticks = 0; diff --git a/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java b/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java index 63476d3..f773e46 100644 --- a/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java +++ b/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java @@ -92,7 +92,7 @@ public class GadgetModule implements Module, Listener { long remaining = durationMs - (now - last); if (remaining > 0L) { long secLeft = (long)Math.ceil((double)remaining / (double)1000.0F); - player.sendMessage("§8[§6Nexus§8] §c" + gadgetName + " §7hat noch §e" + secLeft + "s §7Cooldown."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§c" + gadgetName + " §7hat noch §e" + secLeft + "s §7Cooldown."); player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 0.5F, 0.8F); return false; } else { @@ -142,7 +142,7 @@ public class GadgetModule implements Module, Listener { } else { if (GadgetShield.isAdminShielded(player)) { - player.sendMessage("§8[§6Nexus§8] §cDein Gadget-Schutz ist aktiv. Deaktiviere ihn mit §e/nexus gadgetshield§c."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cDein Gadget-Schutz ist aktiv. Deaktiviere ihn mit §e/nexus gadgetshield§c."); } else { player.sendMessage("§8[§6Parkour§8] §cGadgets können während des Parkours nicht benutzt werden."); } @@ -165,7 +165,7 @@ public class GadgetModule implements Module, Listener { } if (GadgetShield.isProtected(player)) { - player.sendMessage("§8[§6Nexus§8] §cDer Enterhaken ist gerade nicht verfügbar."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cDer Enterhaken ist gerade nicht verfügbar."); return; } @@ -498,7 +498,7 @@ public class GadgetModule implements Module, Listener { } if (spawned) { - player.sendMessage("§8[§6Nexus§8] §dDein Pet wurde gerufen!"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§dDein Pet wurde gerufen!"); player.closeInventory(); } } else if (title.equals("§b§lGadgets §8- §dBegleiter §7(2)")) { @@ -513,7 +513,7 @@ public class GadgetModule implements Module, Listener { } if (spawned) { - player.sendMessage("§8[§6Nexus§8] §dDein Pet wurde gerufen!"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§dDein Pet wurde gerufen!"); player.closeInventory(); } } else if (title.equals("§b§lGadgets §8- §dBegleiter §7(3)")) { @@ -530,7 +530,7 @@ public class GadgetModule implements Module, Listener { } if (spawned) { - player.sendMessage("§8[§6Nexus§8] §dDein Pet wurde gerufen!"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§dDein Pet wurde gerufen!"); player.closeInventory(); } } else if (title.equals("§b§lGadgets §8- §eBallons")) { @@ -540,7 +540,7 @@ public class GadgetModule implements Module, Listener { } this.activeBalloons.put(player.getUniqueId(), new Balloon(player, item.getType())); - player.sendMessage("§8[§6Nexus§8] §aBallon aktiviert!"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aBallon aktiviert!"); player.closeInventory(); } } else if (title.equals("§b§lGadgets §8- §dPartikel")) { @@ -552,12 +552,12 @@ public class GadgetModule implements Module, Listener { this.activeEffects.put(player.getUniqueId(), new ParticleEffect("cloud")); } - player.sendMessage("§8[§6Nexus§8] §aPartikel aktiviert!"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aPartikel aktiviert!"); player.closeInventory(); } else if (title.equals("§b§lGadgets §8- §6Lustiges")) { if (item.getType() == Material.EGG) { ChickenRain.start(player); - player.sendMessage("§8[§6Nexus§8] §fHühnerregen gestartet!"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§fHühnerregen gestartet!"); player.closeInventory(); } else if (item.getType() == Material.FISHING_ROD) { player.getInventory().addItem(new ItemStack[]{this.createItem(Material.FISHING_ROD, "§b§lEnterhaken", "§7Rechtsklick zum Katapultieren §8(3s CD)")}); @@ -574,10 +574,10 @@ public class GadgetModule implements Module, Listener { } else if (item.getType() == Material.SHIELD) { if (this.activeShields.contains(player.getUniqueId())) { this.activeShields.remove(player.getUniqueId()); - player.sendMessage("§8[§6Nexus§8] §cSchutzzone deaktiviert."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cSchutzzone deaktiviert."); } else { this.activeShields.add(player.getUniqueId()); - player.sendMessage("§8[§6Nexus§8] §5Schutzzone aktiviert!"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§5Schutzzone aktiviert!"); } player.closeInventory(); @@ -599,7 +599,7 @@ public class GadgetModule implements Module, Listener { } }, 0L, 1L); player.playSound(player.getLocation(), Sound.ENTITY_PHANTOM_FLAP, 1.0F, 1.2F); - player.sendMessage("§8[§6Nexus§8] §fMini-Tornado: Du wirbelst herum!"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§fMini-Tornado: Du wirbelst herum!"); player.closeInventory(); } else if (item.getType() == Material.TNT) { Location tntLoc = player.getLocation().add((double)0.0F, (double)0.5F, (double)0.0F); @@ -609,7 +609,7 @@ public class GadgetModule implements Module, Listener { player.getWorld().spawnParticle(Particle.EXPLOSION, tntLoc, 2, (double)0.5F, 0.2, (double)0.5F, 0.1); player.getWorld().playSound(tntLoc, Sound.ENTITY_GENERIC_EXPLODE, 1.0F, 1.1F); }, 30L); - player.sendMessage("§8[§6Nexus§8] §cFake-TNT gezündet! Keine Sorge, alles harmlos."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cFake-TNT gezündet! Keine Sorge, alles harmlos."); player.closeInventory(); } else if (item.getType() == Material.SLIME_BALL) { World world = player.getWorld(); @@ -662,7 +662,7 @@ public class GadgetModule implements Module, Listener { } }, 0L, 2L); player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_CELEBRATE, 1.0F, 1.2F); - player.sendMessage("§8[§6Nexus§8] §aMob-Party gestartet! Die Mobs tanzen um dich."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aMob-Party gestartet! Die Mobs tanzen um dich."); player.closeInventory(); } else if (item.getType() == Material.FIREWORK_ROCKET) { for(int i = 0; i < 3; ++i) { @@ -673,7 +673,7 @@ public class GadgetModule implements Module, Listener { }, (long)i * 6L); } - player.sendMessage("§8[§6Nexus§8] §cKonfetti-Kanone: Buntes Konfetti überall!"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cKonfetti-Kanone: Buntes Konfetti überall!"); player.closeInventory(); } else if (item.getType() == Material.NOTE_BLOCK) { Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), new Runnable() { @@ -691,7 +691,7 @@ public class GadgetModule implements Module, Listener { } } }, 0L, 2L); - player.sendMessage("§8[§6Nexus§8] §dTanz-Emote: Du beginnst zu tanzen!"); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§dTanz-Emote: Du beginnst zu tanzen!"); player.closeInventory(); } } @@ -721,7 +721,7 @@ public class GadgetModule implements Module, Listener { 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."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cAlle Gadgets abgelegt."); } private void fillEdges(Inventory inv) { diff --git a/src/main/java/de/nexuslobby/modules/gadgets/GadgetShield.java b/src/main/java/de/nexuslobby/modules/gadgets/GadgetShield.java index 99a4dd2..d49099b 100644 --- a/src/main/java/de/nexuslobby/modules/gadgets/GadgetShield.java +++ b/src/main/java/de/nexuslobby/modules/gadgets/GadgetShield.java @@ -15,12 +15,12 @@ public class GadgetShield { UUID uuid = player.getUniqueId(); if (shielded.contains(uuid)) { shielded.remove(uuid); - player.sendMessage("§8[§6Nexus§8] §cGadget-Schutz §7deaktiviert."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cGadget-Schutz §7deaktiviert."); player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 1.0F); return false; } else { shielded.add(uuid); - player.sendMessage("§8[§6Nexus§8] §aGadget-Schutz §7aktiviert. §8Du bist nun immun gegen Gadgets."); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aGadget-Schutz §7aktiviert. §8Du bist nun immun gegen Gadgets."); player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1.0F, 1.5F); return true; } diff --git a/src/main/java/de/nexuslobby/modules/gadgets/HatManager.java b/src/main/java/de/nexuslobby/modules/gadgets/HatManager.java index e56fb00..ff756c4 100644 --- a/src/main/java/de/nexuslobby/modules/gadgets/HatManager.java +++ b/src/main/java/de/nexuslobby/modules/gadgets/HatManager.java @@ -15,7 +15,7 @@ public class HatManager { } player.getInventory().setHelmet(hat); - player.sendMessage("§8[§6Nexus§8] §aDu trägst nun: " + name); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aDu trägst nun: " + name); } public static void removeHat(Player player) { diff --git a/src/main/java/de/nexuslobby/modules/gadgets/MeteorStrike.java b/src/main/java/de/nexuslobby/modules/gadgets/MeteorStrike.java index 753617e..b5c6aec 100644 --- a/src/main/java/de/nexuslobby/modules/gadgets/MeteorStrike.java +++ b/src/main/java/de/nexuslobby/modules/gadgets/MeteorStrike.java @@ -26,7 +26,7 @@ public class MeteorStrike { if (target != null) { Location finalTarget = target.clone().add((double)0.0F, (double)0.5F, (double)0.0F); finalTarget.getWorld().spawnParticle(Particle.FLAME, finalTarget, 20, (double)0.5F, 0.1, (double)0.5F, 0.05); - shooter.sendMessage("§8[§6Nexus§8] §cMeteorit im Anflug..."); + shooter.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cMeteorit im Anflug..."); Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { finalTarget.getWorld().spawnParticle(Particle.EXPLOSION_EMITTER, finalTarget, 1); finalTarget.getWorld().spawnParticle(Particle.LAVA, finalTarget, 30, (double)0.5F, (double)0.5F, (double)0.5F, 0.1); @@ -36,7 +36,7 @@ public class MeteorStrike { if (entity instanceof Player p) { if (GadgetShield.isProtected(p)) { if (GadgetShield.isAdminShielded(p)) { - p.sendMessage("§8[§6Nexus§8] §7Dein Gadget-Schutzschild hat den Meteoriten abgelenkt!"); + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§7Dein Gadget-Schutzschild hat den Meteoriten abgelenkt!"); } else { p.sendMessage("§8[§6Parkour§8] §7Dein Parkour-Schutzschild hat den Meteoriten abgelenkt!"); } diff --git a/src/main/java/de/nexuslobby/modules/hologram/HoloCommand.java b/src/main/java/de/nexuslobby/modules/hologram/HoloCommand.java index 160787d..9cfdd90 100644 --- a/src/main/java/de/nexuslobby/modules/hologram/HoloCommand.java +++ b/src/main/java/de/nexuslobby/modules/hologram/HoloCommand.java @@ -1,90 +1,90 @@ -package de.nexuslobby.commands; - -import de.nexuslobby.modules.hologram.HologramModule; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class HoloCommand implements CommandExecutor { - - private final HologramModule module; - - public HoloCommand(HologramModule module) { - this.module = module; - } - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player player)) return true; - - if (!player.hasPermission("nexuslobby.hologram")) { - player.sendMessage("§8[§6Nexus§8] §cKeine Rechte."); - return true; - } - - if (args.length < 1) { - sendHelp(player); - return true; - } - - if (args[0].equalsIgnoreCase("create")) { - // Wir brauchen id, typ und mindestens ein Wort Text - if (args.length < 4) { - player.sendMessage("§cNutze: /holo create "); - return true; - } - - String id = args[1]; - // Der Typ (args[2]) wird im neuen System ignoriert, da wir Seiten nutzen - - StringBuilder sb = new StringBuilder(); - for (int i = 3; i < args.length; i++) { - sb.append(args[i]).append(i == args.length - 1 ? "" : " "); - } - - String fullText = sb.toString().trim(); - List pages = new ArrayList<>(); - - // Support für mehrere Seiten via ";" - if (fullText.contains(";")) { - pages.addAll(Arrays.asList(fullText.split(";"))); - } else { - pages.add(fullText); - } - - // Aufruf der neuen Methode mit List - module.createHologram(id, player.getLocation(), pages); - player.sendMessage("§8[§6Nexus§8] §aHologramm §e" + id + " §aerstellt (" + pages.size() + " Seiten)."); - } - else if (args[0].equalsIgnoreCase("delete")) { - if (args.length < 2) { - player.sendMessage("§cBitte gib eine ID an: /holo delete "); - return true; - } - module.removeHologram(args[1]); - player.sendMessage("§8[§6Nexus§8] §cHologramm §e" + args[1] + " §agelöscht."); - } - else if (args[0].equalsIgnoreCase("reload")) { - module.reloadHolograms(); - player.sendMessage("§8[§6Nexus§8] §aHologramme neu geladen."); - } else { - sendHelp(player); - } - - return true; - } - - private void sendHelp(Player player) { - player.sendMessage("§8§m-----------§r §6Hologramme §8§m-----------"); - player.sendMessage("§e/holo create "); - player.sendMessage("§e/holo delete "); - player.sendMessage("§e/holo reload"); - player.sendMessage("§7Nutze §b; §7für neue Seiten."); - player.sendMessage("§7Nutze §b\\n §7für Zeilenumbruch."); - } +package de.nexuslobby.commands; + +import de.nexuslobby.modules.hologram.HologramModule; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class HoloCommand implements CommandExecutor { + + private final HologramModule module; + + public HoloCommand(HologramModule module) { + this.module = module; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) return true; + + if (!player.hasPermission("nexuslobby.hologram")) { + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cKeine Rechte."); + return true; + } + + if (args.length < 1) { + sendHelp(player); + return true; + } + + if (args[0].equalsIgnoreCase("create")) { + // Wir brauchen id, typ und mindestens ein Wort Text + if (args.length < 4) { + player.sendMessage("§cNutze: /holo create "); + return true; + } + + String id = args[1]; + // Der Typ (args[2]) wird im neuen System ignoriert, da wir Seiten nutzen + + StringBuilder sb = new StringBuilder(); + for (int i = 3; i < args.length; i++) { + sb.append(args[i]).append(i == args.length - 1 ? "" : " "); + } + + String fullText = sb.toString().trim(); + List pages = new ArrayList<>(); + + // Support für mehrere Seiten via ";" + if (fullText.contains(";")) { + pages.addAll(Arrays.asList(fullText.split(";"))); + } else { + pages.add(fullText); + } + + // Aufruf der neuen Methode mit List + module.createHologram(id, player.getLocation(), pages); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aHologramm §e" + id + " §aerstellt (" + pages.size() + " Seiten)."); + } + else if (args[0].equalsIgnoreCase("delete")) { + if (args.length < 2) { + player.sendMessage("§cBitte gib eine ID an: /holo delete "); + return true; + } + module.removeHologram(args[1]); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cHologramm §e" + args[1] + " §agelöscht."); + } + else if (args[0].equalsIgnoreCase("reload")) { + module.reloadHolograms(); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aHologramme neu geladen."); + } else { + sendHelp(player); + } + + return true; + } + + private void sendHelp(Player player) { + player.sendMessage("§8§m-----------§r §6Hologramme §8§m-----------"); + player.sendMessage("§e/holo create "); + player.sendMessage("§e/holo delete "); + player.sendMessage("§e/holo reload"); + player.sendMessage("§7Nutze §b; §7für neue Seiten."); + player.sendMessage("§7Nutze §b\\n §7für Zeilenumbruch."); + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/hologram/HologramModule.java b/src/main/java/de/nexuslobby/modules/hologram/HologramModule.java index af55301..622c893 100644 --- a/src/main/java/de/nexuslobby/modules/hologram/HologramModule.java +++ b/src/main/java/de/nexuslobby/modules/hologram/HologramModule.java @@ -1,221 +1,221 @@ -package de.nexuslobby.modules.hologram; - -import de.nexuslobby.NexusLobby; -import de.nexuslobby.api.Module; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Interaction; -import org.bukkit.entity.Player; -import org.bukkit.entity.TextDisplay; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerChangedWorldEvent; -import org.bukkit.event.player.PlayerInteractEntityEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -public class HologramModule implements Module, Listener { - - private final Map holograms = new ConcurrentHashMap<>(); - private File file; - private FileConfiguration config; - - @Override - public String getName() { return "Holograms"; } - - @Override - public void onEnable() { - loadConfig(); - loadHolograms(); - Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); - - // Render-Task: Prüft alle 5 Ticks Sichtbarkeit und Placeholder-Updates - Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), () -> { - for (Player player : Bukkit.getOnlinePlayers()) { - holograms.values().forEach(h -> h.renderForPlayer(player)); - } - }, 20L, 5L); - } - - private void loadConfig() { - file = new File(NexusLobby.getInstance().getDataFolder(), "holograms.yml"); - config = YamlConfiguration.loadConfiguration(file); - } - - private void loadHolograms() { - // Vorherige Instanzen säubern - holograms.values().forEach(NexusHologram::removeAll); - holograms.clear(); - - for (String id : config.getKeys(false)) { - String worldName = config.getString(id + ".world"); - if (worldName == null) continue; - - World world = Bukkit.getWorld(worldName); - if (world == null) continue; - - Location loc = new Location(world, - config.getDouble(id + ".x"), - config.getDouble(id + ".y"), - config.getDouble(id + ".z")); - - List pages; - if (config.isList(id + ".text")) { - pages = config.getStringList(id + ".text"); - } else { - pages = new ArrayList<>(); - pages.add(config.getString(id + ".text", "No Text")); - } - - holograms.put(id, new NexusHologram(id, loc, pages)); - } - } - - @EventHandler - public void onInteract(PlayerInteractEntityEvent event) { - // Nur auf Interaction-Entities reagieren (Hologramm-Hitboxen) - if (!(event.getRightClicked() instanceof Interaction)) return; - - for (NexusHologram holo : holograms.values()) { - if (holo.isInteractionEntity(event.getRightClicked().getUniqueId())) { - holo.nextPage(event.getPlayer()); - break; - } - } - } - - @EventHandler - public void onQuit(PlayerQuitEvent event) { - holograms.values().forEach(h -> h.removeForPlayer(event.getPlayer())); - } - - @EventHandler - public void onWorldChange(PlayerChangedWorldEvent event) { - holograms.values().forEach(h -> h.removeForPlayer(event.getPlayer())); - } - - @EventHandler - public void onJoin(PlayerJoinEvent event) { - // Cleanup alter Entity-Reste, die eventuell noch in der Welt schweben - Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { - Player p = event.getPlayer(); - for (Entity entity : p.getWorld().getEntities()) { - if (entity.getCustomName() != null && entity.getCustomName().startsWith("nexus_h_")) { - // Wenn das Hologramm nicht exakt für diesen Spieler benannt ist -> verstecken - if (!entity.getCustomName().endsWith("_" + p.getName())) { - p.hideEntity(NexusLobby.getInstance(), entity); - } - } - } - }, 10L); - } - - public void createHologram(String id, Location loc, List pages) { - // Falls ID bereits existiert, altes Hologramm sauber entfernen - if (holograms.containsKey(id)) { - removeHologram(id); - } - - config.set(id + ".world", loc.getWorld().getName()); - config.set(id + ".x", loc.getX()); - config.set(id + ".y", loc.getY()); - config.set(id + ".z", loc.getZ()); - config.set(id + ".text", pages); - saveHoloConfig(); - - NexusHologram holo = new NexusHologram(id, loc, pages); - holograms.put(id, holo); - } - - public void removeHologram(String id) { - NexusHologram holo = holograms.remove(id); - if (holo != null) { - // Erst für alle Spieler visuell entfernen - for (Player player : Bukkit.getOnlinePlayers()) { - holo.removeForPlayer(player); - } - // Dann Entities serverseitig löschen - holo.removeAll(); - } - config.set(id, null); - saveHoloConfig(); - } - - /** - * Lädt die holograms.yml neu und rendert alle Hologramme für Online-Spieler neu. - * Wird von HoloCommand (/holo reload) aufgerufen. - */ - public void reloadHolograms() { - // Alle aktiven Hologramme für alle Spieler entfernen - for (Player player : Bukkit.getOnlinePlayers()) { - holograms.values().forEach(h -> h.removeForPlayer(player)); - } - // Config neu einlesen und Hologramme neu laden - loadConfig(); - loadHolograms(); - } - - private void saveHoloConfig() { - try { - // Sicherstellen, dass Placeholder-Strings (z.B. %bungeetotal%) korrekt - // gequotet gespeichert werden, damit SnakeYAML sie nicht als YAML-Direktive - // missinterpretiert. Dazu speichern wir die text-Listen explizit als - // String-Werte mit einfachen Anführungszeichen via eigener Serialisierung. - StringBuilder yaml = new StringBuilder(); - for (String id : config.getKeys(false)) { - yaml.append(id).append(":\n"); - yaml.append(" world: ").append(quoteIfNeeded(config.getString(id + ".world", ""))).append("\n"); - yaml.append(" x: ").append(config.getDouble(id + ".x")).append("\n"); - yaml.append(" y: ").append(config.getDouble(id + ".y")).append("\n"); - yaml.append(" z: ").append(config.getDouble(id + ".z")).append("\n"); - yaml.append(" text:\n"); - for (String line : config.getStringList(id + ".text")) { - yaml.append(" - ").append(quoteIfNeeded(line)).append("\n"); - } - } - java.nio.file.Files.writeString(file.toPath(), yaml.toString()); - } catch (IOException e) { - NexusLobby.getInstance().getLogger().severe("Konnte holograms.yml nicht speichern!"); - NexusLobby.getInstance().getLogger().severe("Fehler beim Speichern der Hologramme: " + e.getMessage()); - } - } - - /** - * Umschließt Strings mit einfachen Anführungszeichen, wenn sie YAML-Sonderzeichen - * wie '%', ':', '#' enthalten, um Parsing-Fehler zu vermeiden. - */ - private String quoteIfNeeded(String value) { - if (value == null) return "''"; - if (value.contains("%") || value.contains(":") || value.contains("#") || value.contains("'")) { - return "'" + value.replace("'", "''") + "'"; - } - return value; - } - - /** - * WICHTIG: Diese Methode wird vom LobbyTabCompleter benötigt! - * @return Set aller registrierten Hologramm-IDs - */ - public Set getHologramIds() { - return holograms.keySet(); - } - - @Override - public void onDisable() { - org.bukkit.event.HandlerList.unregisterAll(this); - holograms.values().forEach(NexusHologram::removeAll); - holograms.clear(); - } +package de.nexuslobby.modules.hologram; + +import de.nexuslobby.NexusLobby; +import de.nexuslobby.api.Module; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Interaction; +import org.bukkit.entity.Player; +import org.bukkit.entity.TextDisplay; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class HologramModule implements Module, Listener { + + private final Map holograms = new ConcurrentHashMap<>(); + private File file; + private FileConfiguration config; + + @Override + public String getName() { return "Holograms"; } + + @Override + public void onEnable() { + loadConfig(); + loadHolograms(); + Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); + + // Render-Task: Prüft alle 5 Ticks Sichtbarkeit und Placeholder-Updates + Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), () -> { + for (Player player : Bukkit.getOnlinePlayers()) { + holograms.values().forEach(h -> h.renderForPlayer(player)); + } + }, 20L, 5L); + } + + private void loadConfig() { + file = new File(NexusLobby.getInstance().getDataFolder(), "holograms.yml"); + config = YamlConfiguration.loadConfiguration(file); + } + + private void loadHolograms() { + // Vorherige Instanzen säubern + holograms.values().forEach(NexusHologram::removeAll); + holograms.clear(); + + for (String id : config.getKeys(false)) { + String worldName = config.getString(id + ".world"); + if (worldName == null) continue; + + World world = Bukkit.getWorld(worldName); + if (world == null) continue; + + Location loc = new Location(world, + config.getDouble(id + ".x"), + config.getDouble(id + ".y"), + config.getDouble(id + ".z")); + + List pages; + if (config.isList(id + ".text")) { + pages = config.getStringList(id + ".text"); + } else { + pages = new ArrayList<>(); + pages.add(config.getString(id + ".text", "No Text")); + } + + holograms.put(id, new NexusHologram(id, loc, pages)); + } + } + + @EventHandler + public void onInteract(PlayerInteractEntityEvent event) { + // Nur auf Interaction-Entities reagieren (Hologramm-Hitboxen) + if (!(event.getRightClicked() instanceof Interaction)) return; + + for (NexusHologram holo : holograms.values()) { + if (holo.isInteractionEntity(event.getRightClicked().getUniqueId())) { + holo.nextPage(event.getPlayer()); + break; + } + } + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + holograms.values().forEach(h -> h.removeForPlayer(event.getPlayer())); + } + + @EventHandler + public void onWorldChange(PlayerChangedWorldEvent event) { + holograms.values().forEach(h -> h.removeForPlayer(event.getPlayer())); + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + // Cleanup alter Entity-Reste, die eventuell noch in der Welt schweben + Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { + Player p = event.getPlayer(); + for (Entity entity : p.getWorld().getEntities()) { + if (entity.getCustomName() != null && entity.getCustomName().startsWith("nexus_h_")) { + // Wenn das Hologramm nicht exakt für diesen Spieler benannt ist -> verstecken + if (!entity.getCustomName().endsWith("_" + p.getName())) { + p.hideEntity(NexusLobby.getInstance(), entity); + } + } + } + }, 10L); + } + + public void createHologram(String id, Location loc, List pages) { + // Falls ID bereits existiert, altes Hologramm sauber entfernen + if (holograms.containsKey(id)) { + removeHologram(id); + } + + config.set(id + ".world", loc.getWorld().getName()); + config.set(id + ".x", loc.getX()); + config.set(id + ".y", loc.getY()); + config.set(id + ".z", loc.getZ()); + config.set(id + ".text", pages); + saveHoloConfig(); + + NexusHologram holo = new NexusHologram(id, loc, pages); + holograms.put(id, holo); + } + + public void removeHologram(String id) { + NexusHologram holo = holograms.remove(id); + if (holo != null) { + // Erst für alle Spieler visuell entfernen + for (Player player : Bukkit.getOnlinePlayers()) { + holo.removeForPlayer(player); + } + // Dann Entities serverseitig löschen + holo.removeAll(); + } + config.set(id, null); + saveHoloConfig(); + } + + /** + * Lädt die holograms.yml neu und rendert alle Hologramme für Online-Spieler neu. + * Wird von HoloCommand (/holo reload) aufgerufen. + */ + public void reloadHolograms() { + // Alle aktiven Hologramme für alle Spieler entfernen + for (Player player : Bukkit.getOnlinePlayers()) { + holograms.values().forEach(h -> h.removeForPlayer(player)); + } + // Config neu einlesen und Hologramme neu laden + loadConfig(); + loadHolograms(); + } + + private void saveHoloConfig() { + try { + // Sicherstellen, dass Placeholder-Strings (z.B. %bungeetotal%) korrekt + // gequotet gespeichert werden, damit SnakeYAML sie nicht als YAML-Direktive + // missinterpretiert. Dazu speichern wir die text-Listen explizit als + // String-Werte mit einfachen Anführungszeichen via eigener Serialisierung. + StringBuilder yaml = new StringBuilder(); + for (String id : config.getKeys(false)) { + yaml.append(id).append(":\n"); + yaml.append(" world: ").append(quoteIfNeeded(config.getString(id + ".world", ""))).append("\n"); + yaml.append(" x: ").append(config.getDouble(id + ".x")).append("\n"); + yaml.append(" y: ").append(config.getDouble(id + ".y")).append("\n"); + yaml.append(" z: ").append(config.getDouble(id + ".z")).append("\n"); + yaml.append(" text:\n"); + for (String line : config.getStringList(id + ".text")) { + yaml.append(" - ").append(quoteIfNeeded(line)).append("\n"); + } + } + java.nio.file.Files.writeString(file.toPath(), yaml.toString()); + } catch (IOException e) { + NexusLobby.getInstance().getLogger().severe("Konnte holograms.yml nicht speichern!"); + NexusLobby.getInstance().getLogger().severe("Fehler beim Speichern der Hologramme: " + e.getMessage()); + } + } + + /** + * Umschließt Strings mit einfachen Anführungszeichen, wenn sie YAML-Sonderzeichen + * wie '%', ':', '#' enthalten, um Parsing-Fehler zu vermeiden. + */ + private String quoteIfNeeded(String value) { + if (value == null) return "''"; + if (value.contains("%") || value.contains(":") || value.contains("#") || value.contains("'")) { + return "'" + value.replace("'", "''") + "'"; + } + return value; + } + + /** + * WICHTIG: Diese Methode wird vom LobbyTabCompleter benötigt! + * @return Set aller registrierten Hologramm-IDs + */ + public Set getHologramIds() { + return holograms.keySet(); + } + + @Override + public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); + holograms.values().forEach(NexusHologram::removeAll); + holograms.clear(); + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/hologram/NexusHologram.java b/src/main/java/de/nexuslobby/modules/hologram/NexusHologram.java index efd9710..cf06dcf 100644 --- a/src/main/java/de/nexuslobby/modules/hologram/NexusHologram.java +++ b/src/main/java/de/nexuslobby/modules/hologram/NexusHologram.java @@ -1,118 +1,118 @@ -package de.nexuslobby.modules.hologram; - -import de.nexuslobby.NexusLobby; -import me.clip.placeholderapi.PlaceholderAPI; -import org.bukkit.Bukkit; -import org.bukkit.Color; -import org.bukkit.Location; -import org.bukkit.entity.Display; -import org.bukkit.entity.Interaction; -import org.bukkit.entity.Player; -import org.bukkit.entity.TextDisplay; - -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -public class NexusHologram { - - private final String id; - private final Location location; - private final List pages; - - private final Map playerEntities = new ConcurrentHashMap<>(); - private final Map playerInteractions = new ConcurrentHashMap<>(); - private final Map currentPage = new ConcurrentHashMap<>(); - - public NexusHologram(String id, Location location, List pages) { - this.id = id; - this.location = location; - this.pages = pages; - } - - public void nextPage(Player player) { - if (pages.size() <= 1) return; - int next = (currentPage.getOrDefault(player.getUniqueId(), 0) + 1) % pages.size(); - currentPage.put(player.getUniqueId(), next); - renderForPlayer(player); - } - - public void renderForPlayer(Player player) { - if (!player.getWorld().equals(location.getWorld()) || player.getLocation().distanceSquared(location) > 2304) { - removeForPlayer(player); - return; - } - - int pageIdx = currentPage.getOrDefault(player.getUniqueId(), 0); - if (pageIdx >= pages.size()) pageIdx = 0; - - String rawText = pages.get(pageIdx); - if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { - rawText = PlaceholderAPI.setPlaceholders(player, rawText); - } - final String finalText = rawText.replace("&", "§").replace("\\n", "\n"); - - TextDisplay display = playerEntities.get(player.getUniqueId()); - - if (display == null || !display.isValid()) { - // Text-Display erstellen (Hier lassen wir den Namen zur internen Identifikation, - // aber schalten ihn strikt unsichtbar) - display = location.getWorld().spawn(location, TextDisplay.class, entity -> { - entity.setCustomName("nexus_h_" + id + "_" + player.getName()); - entity.setCustomNameVisible(false); - entity.setPersistent(false); - entity.setBillboard(Display.Billboard.CENTER); - entity.setBackgroundColor(Color.fromARGB(0, 0, 0, 0)); - entity.setText(finalText); - entity.setInvulnerable(true); - }); - - // Interaction Entity (Hitbox) erstellen - // FIX: WIR SETZEN KEINEN CUSTOM NAME MEHR. - // Das verhindert zu 100%, dass Minecraft etwas anzeigt. - Interaction interact = location.getWorld().spawn(location, Interaction.class, entity -> { - entity.setInteractionWidth(2.5f); - entity.setInteractionHeight(2.0f); - entity.setCustomNameVisible(false); - entity.setPersistent(false); - }); - - // Nur für den Zielspieler sichtbar machen - for (Player other : Bukkit.getOnlinePlayers()) { - if (!other.getUniqueId().equals(player.getUniqueId())) { - other.hideEntity(NexusLobby.getInstance(), display); - other.hideEntity(NexusLobby.getInstance(), interact); - } - } - - playerEntities.put(player.getUniqueId(), display); - playerInteractions.put(player.getUniqueId(), interact); - } else { - if (!display.getText().equals(finalText)) { - display.setText(finalText); - } - } - } - - public void removeForPlayer(Player player) { - TextDisplay display = playerEntities.remove(player.getUniqueId()); - if (display != null) display.remove(); - - Interaction interact = playerInteractions.remove(player.getUniqueId()); - if (interact != null) interact.remove(); - } - - public void removeAll() { - playerEntities.values().forEach(TextDisplay::remove); - playerInteractions.values().forEach(Interaction::remove); - playerEntities.clear(); - playerInteractions.clear(); - currentPage.clear(); - } - - public boolean isInteractionEntity(UUID entityId) { - // Da wir keinen Namen mehr nutzen, verlassen wir uns rein auf die UUID in der Map - return playerInteractions.values().stream().anyMatch(i -> i.getUniqueId().equals(entityId)); - } +package de.nexuslobby.modules.hologram; + +import de.nexuslobby.NexusLobby; +import me.clip.placeholderapi.PlaceholderAPI; +import org.bukkit.Bukkit; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.entity.Display; +import org.bukkit.entity.Interaction; +import org.bukkit.entity.Player; +import org.bukkit.entity.TextDisplay; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class NexusHologram { + + private final String id; + private final Location location; + private final List pages; + + private final Map playerEntities = new ConcurrentHashMap<>(); + private final Map playerInteractions = new ConcurrentHashMap<>(); + private final Map currentPage = new ConcurrentHashMap<>(); + + public NexusHologram(String id, Location location, List pages) { + this.id = id; + this.location = location; + this.pages = pages; + } + + public void nextPage(Player player) { + if (pages.size() <= 1) return; + int next = (currentPage.getOrDefault(player.getUniqueId(), 0) + 1) % pages.size(); + currentPage.put(player.getUniqueId(), next); + renderForPlayer(player); + } + + public void renderForPlayer(Player player) { + if (!player.getWorld().equals(location.getWorld()) || player.getLocation().distanceSquared(location) > 2304) { + removeForPlayer(player); + return; + } + + int pageIdx = currentPage.getOrDefault(player.getUniqueId(), 0); + if (pageIdx >= pages.size()) pageIdx = 0; + + String rawText = pages.get(pageIdx); + if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + rawText = PlaceholderAPI.setPlaceholders(player, rawText); + } + final String finalText = rawText.replace("&", "§").replace("\\n", "\n"); + + TextDisplay display = playerEntities.get(player.getUniqueId()); + + if (display == null || !display.isValid()) { + // Text-Display erstellen (Hier lassen wir den Namen zur internen Identifikation, + // aber schalten ihn strikt unsichtbar) + display = location.getWorld().spawn(location, TextDisplay.class, entity -> { + entity.setCustomName("nexus_h_" + id + "_" + player.getName()); + entity.setCustomNameVisible(false); + entity.setPersistent(false); + entity.setBillboard(Display.Billboard.CENTER); + entity.setBackgroundColor(Color.fromARGB(0, 0, 0, 0)); + entity.setText(finalText); + entity.setInvulnerable(true); + }); + + // Interaction Entity (Hitbox) erstellen + // FIX: WIR SETZEN KEINEN CUSTOM NAME MEHR. + // Das verhindert zu 100%, dass Minecraft etwas anzeigt. + Interaction interact = location.getWorld().spawn(location, Interaction.class, entity -> { + entity.setInteractionWidth(2.5f); + entity.setInteractionHeight(2.0f); + entity.setCustomNameVisible(false); + entity.setPersistent(false); + }); + + // Nur für den Zielspieler sichtbar machen + for (Player other : Bukkit.getOnlinePlayers()) { + if (!other.getUniqueId().equals(player.getUniqueId())) { + other.hideEntity(NexusLobby.getInstance(), display); + other.hideEntity(NexusLobby.getInstance(), interact); + } + } + + playerEntities.put(player.getUniqueId(), display); + playerInteractions.put(player.getUniqueId(), interact); + } else { + if (!display.getText().equals(finalText)) { + display.setText(finalText); + } + } + } + + public void removeForPlayer(Player player) { + TextDisplay display = playerEntities.remove(player.getUniqueId()); + if (display != null) display.remove(); + + Interaction interact = playerInteractions.remove(player.getUniqueId()); + if (interact != null) interact.remove(); + } + + public void removeAll() { + playerEntities.values().forEach(TextDisplay::remove); + playerInteractions.values().forEach(Interaction::remove); + playerEntities.clear(); + playerInteractions.clear(); + currentPage.clear(); + } + + public boolean isInteractionEntity(UUID entityId) { + // Da wir keinen Namen mehr nutzen, verlassen wir uns rein auf die UUID in der Map + return playerInteractions.values().stream().anyMatch(i -> i.getUniqueId().equals(entityId)); + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/intro/IntroModule.java b/src/main/java/de/nexuslobby/modules/intro/IntroModule.java index 7848408..756b240 100644 --- a/src/main/java/de/nexuslobby/modules/intro/IntroModule.java +++ b/src/main/java/de/nexuslobby/modules/intro/IntroModule.java @@ -1,365 +1,365 @@ -package de.nexuslobby.modules.intro; - -import de.nexuslobby.NexusLobby; -import de.nexuslobby.api.Module; -import org.bukkit.Bukkit; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.scheduler.BukkitRunnable; -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.io.IOException; -import java.util.*; - -public class IntroModule implements Module, Listener, CommandExecutor { - - private final Set activeIntro = new HashSet<>(); - - private final List points = new ArrayList<>(); - private final List labels = new ArrayList<>(); - - // Abschluss-Titel (konfigurierbar) - private String endTitle = "§a§lWillkommen!"; - private String endSubtitle = "§7Viel Spaß auf NexusLobby"; - - private File configFile; - private FileConfiguration config; - - // --- Einstellungen --- - private final int TICKS_FLUG = 70; - private final int TICKS_PAUSE = 30; - - private final int TITLE_FADE_IN = 10; - private final int TITLE_STAY = 50; - private final int TITLE_FADE_OUT = 10; - - @Override - public String getName() { return "Intro"; } - - @Override - public void onEnable() { - Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); - if (NexusLobby.getInstance().getCommand("intro") != null) { - NexusLobby.getInstance().getCommand("intro").setExecutor(this); - } - loadConfig(); - } - - @Override - public void onDisable() { - org.bukkit.event.HandlerList.unregisterAll(this); - activeIntro.clear(); - } - - // ───────────────────────────────────────────────────────────────────────── - // Persistenz - // ───────────────────────────────────────────────────────────────────────── - - private void loadConfig() { - points.clear(); - labels.clear(); - configFile = new File(NexusLobby.getInstance().getDataFolder(), "intro.yml"); - if (!configFile.exists()) { - try { configFile.createNewFile(); } catch (IOException ignored) {} - } - config = YamlConfiguration.loadConfiguration(configFile); - - // Abschluss-Titel laden - endTitle = config.getString("end-title", "§a§lWillkommen!"); - endSubtitle = config.getString("end-subtitle", "§7Viel Spaß auf NexusLobby"); - - // Punkte laden - List list = config.getList("intro-points"); - if (list != null) { - for (Object obj : list) { - if (obj instanceof Map map) { - Object locObj = map.get("location"); - Object labelObj = map.get("label"); - if (locObj instanceof Location loc) { - points.add(loc); - labels.add(labelObj != null ? labelObj.toString() : ""); - } - } - } - } - - // Rückwärts-Kompatibilität - if (points.isEmpty()) { - List oldList = config.getList("points"); - if (oldList != null) { - for (Object obj : oldList) { - if (obj instanceof Location loc) { - points.add(loc); - labels.add(""); - } - } - } - } - } - - private void saveConfig() { - // Abschluss-Titel speichern - config.set("end-title", endTitle); - config.set("end-subtitle", endSubtitle); - - // Punkte speichern - List> data = new ArrayList<>(); - for (int i = 0; i < points.size(); i++) { - Map entry = new LinkedHashMap<>(); - entry.put("location", points.get(i)); - entry.put("label", labels.get(i)); - data.add(entry); - } - config.set("intro-points", data); - config.set("points", null); - - try { - config.save(configFile); - } catch (IOException e) { - NexusLobby.getInstance().getLogger().severe("Fehler beim Speichern der Intro-Config: " + e.getMessage()); - } - } - - // ───────────────────────────────────────────────────────────────────────── - // Events - // ───────────────────────────────────────────────────────────────────────── - - @EventHandler - public void onJoin(PlayerJoinEvent event) { - if (!event.getPlayer().hasPlayedBefore() && points.size() >= 2) { - startIntro(event.getPlayer()); - } - } - - // ───────────────────────────────────────────────────────────────────────── - // Befehle - // ───────────────────────────────────────────────────────────────────────── - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { - if (!(sender instanceof Player p)) return true; - if (!p.hasPermission("nexuslobby.admin")) return true; - - if (args.length == 0) { - sendHelp(p); - return true; - } - - switch (args[0].toLowerCase()) { - - case "add" -> { - String pointLabel = args.length > 1 - ? String.join(" ", Arrays.copyOfRange(args, 1, args.length)) - : ""; - points.add(p.getLocation()); - labels.add(pointLabel); - saveConfig(); - if (pointLabel.isEmpty()) { - p.sendMessage("§8[§6Nexus§8] §aPunkt §e#" + points.size() + " §awurde gesetzt."); - } else { - p.sendMessage("§8[§6Nexus§8] §aPunkt §e#" + points.size() + " §amit Text §e\"" + pointLabel + "\" §agesetzt."); - } - } - - case "clear" -> { - points.clear(); - labels.clear(); - saveConfig(); - p.sendMessage("§8[§6Nexus§8] §cAlle Intro-Punkte wurden gelöscht."); - } - - case "list" -> { - if (points.isEmpty()) { - p.sendMessage("§8[§6Nexus§8] §cKeine Punkte vorhanden."); - return true; - } - p.sendMessage("§8§m------------------------------------"); - p.sendMessage("§6§lIntro-Punkte (" + points.size() + ")"); - for (int i = 0; i < points.size(); i++) { - Location loc = points.get(i); - String lbl = labels.get(i).isEmpty() ? "§7(kein Text)" : "§e" + labels.get(i); - p.sendMessage("§8#" + (i + 1) + " §7" - + String.format("%.1f", loc.getX()) + ", " - + String.format("%.1f", loc.getY()) + ", " - + String.format("%.1f", loc.getZ()) - + " " + lbl); - } - p.sendMessage("§8§m------------------------------------"); - } - - case "start" -> { - if (points.size() < 2) { - p.sendMessage("§8[§6Nexus§8] §cDu brauchst mindestens 2 Punkte für eine Fahrt."); - } else { - startIntro(p); - } - } - - // /intro setendtitle — setzt den großen Abschluss-Titel - case "setendtitle" -> { - if (args.length < 2) { - p.sendMessage("§8[§6Nexus§8] §cBenutzung: §e/intro setendtitle "); - p.sendMessage("§7Farbcodes mit §e& §7möglich. Beispiel: §e/intro setendtitle &a&lWillkommen!"); - return true; - } - endTitle = colorize(String.join(" ", Arrays.copyOfRange(args, 1, args.length))); - saveConfig(); - p.sendMessage("§8[§6Nexus§8] §aAbschluss-Titel gesetzt: " + endTitle); - // Vorschau - p.sendTitle(endTitle, endSubtitle, TITLE_FADE_IN, TITLE_STAY, TITLE_FADE_OUT); - } - - // /intro setendsubtitle — setzt den kleinen Abschluss-Untertitel - case "setendsubtitle" -> { - if (args.length < 2) { - p.sendMessage("§8[§6Nexus§8] §cBenutzung: §e/intro setendsubtitle "); - p.sendMessage("§7Farbcodes mit §e& §7möglich. Beispiel: §e/intro setendsubtitle &7Viel Spaß!"); - return true; - } - endSubtitle = colorize(String.join(" ", Arrays.copyOfRange(args, 1, args.length))); - saveConfig(); - p.sendMessage("§8[§6Nexus§8] §aAbschluss-Untertitel gesetzt: " + endSubtitle); - // Vorschau - p.sendTitle(endTitle, endSubtitle, TITLE_FADE_IN, TITLE_STAY, TITLE_FADE_OUT); - } - - default -> sendHelp(p); - } - return true; - } - - private void sendHelp(Player p) { - p.sendMessage("§8§m------------------------------------"); - p.sendMessage("§6§lNexus Intro System (Cinematic)"); - p.sendMessage("§e/intro add [Text] §7- Punkt + optionaler Titel"); - p.sendMessage("§e/intro clear §7- Alle Punkte löschen"); - p.sendMessage("§e/intro list §7- Alle Punkte anzeigen"); - p.sendMessage("§e/intro start §7- Fahrt testen"); - p.sendMessage("§e/intro setendtitle §7- Abschluss-Titel setzen"); - p.sendMessage("§e/intro setendsubtitle §7- Abschluss-Untertitel setzen"); - p.sendMessage("§7Aktuell: §r" + endTitle + " §8/ §r" + endSubtitle); - p.sendMessage("§8§m------------------------------------"); - } - - // ───────────────────────────────────────────────────────────────────────── - // Intro-Logik - // ───────────────────────────────────────────────────────────────────────── - - public void startIntro(Player player) { - activeIntro.add(player.getUniqueId()); - player.setGameMode(GameMode.SPECTATOR); - - // Titel für den ersten Punkt sofort anzeigen - showTitleForSegment(player, 0); - - new BukkitRunnable() { - int currentSegment = 0; - int tickInSegment = 0; - boolean isPausing = true; - int lastTitleSegment = 0; - - @Override - public void run() { - try { - if (!player.isOnline() || !activeIntro.contains(player.getUniqueId())) { - this.cancel(); - return; - } - - if (currentSegment >= points.size() - 1) { - stopIntro(player); - this.cancel(); - return; - } - - Location start = points.get(currentSegment); - Location end = points.get(currentSegment + 1); - - if (isPausing) { - player.teleport(start); - tickInSegment++; - if (tickInSegment >= TICKS_PAUSE) { - isPausing = false; - tickInSegment = 0; - } - } else { - double progress = (double) tickInSegment / (double) TICKS_FLUG; - double smoothT = progress * progress * (3 - 2 * progress); - - player.teleport(interpolate(start, end, smoothT)); - - tickInSegment++; - if (tickInSegment >= TICKS_FLUG) { - isPausing = true; - tickInSegment = 0; - currentSegment++; - - if (currentSegment < points.size() && currentSegment != lastTitleSegment) { - showTitleForSegment(player, currentSegment); - lastTitleSegment = currentSegment; - } - } - } - - } catch (Exception e) { - this.cancel(); - stopIntro(player); - } - } - }.runTaskTimer(NexusLobby.getInstance(), 0L, 1L); - } - - private void showTitleForSegment(Player player, int segmentIndex) { - if (segmentIndex < 0 || segmentIndex >= labels.size()) return; - String lbl = labels.get(segmentIndex); - if (lbl == null || lbl.isEmpty()) return; - - player.sendTitle( - "§6§l" + lbl, - "§7✦ NexusLobby ✦", - TITLE_FADE_IN, - TITLE_STAY, - TITLE_FADE_OUT - ); - } - - private Location interpolate(Location start, Location end, double t) { - double x = start.getX() + (end.getX() - start.getX()) * t; - double y = start.getY() + (end.getY() - start.getY()) * t; - double z = start.getZ() + (end.getZ() - start.getZ()) * t; - - float startYaw = start.getYaw(); - float endYaw = end.getYaw(); - float diff = (endYaw - startYaw) % 360; - if (diff > 180) diff -= 360; - if (diff < -180) diff += 360; - float yaw = startYaw + diff * (float) t; - float pitch = (float) (start.getPitch() + (end.getPitch() - start.getPitch()) * t); - - return new Location(start.getWorld(), x, y, z, yaw, pitch); - } - - private void stopIntro(Player player) { - activeIntro.remove(player.getUniqueId()); - player.setGameMode(GameMode.ADVENTURE); - player.teleport(player.getWorld().getSpawnLocation()); - - // Abschluss-Titel — komplett konfigurierbar - player.sendTitle(endTitle, endSubtitle, 10, 60, 20); - } - - /** Ersetzt &-Farbcodes durch §-Codes */ - private String colorize(String input) { - return input.replace("&", "§"); - } +package de.nexuslobby.modules.intro; + +import de.nexuslobby.NexusLobby; +import de.nexuslobby.api.Module; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.scheduler.BukkitRunnable; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +public class IntroModule implements Module, Listener, CommandExecutor { + + private final Set activeIntro = new HashSet<>(); + + private final List points = new ArrayList<>(); + private final List labels = new ArrayList<>(); + + // Abschluss-Titel (konfigurierbar) + private String endTitle = "§a§lWillkommen!"; + private String endSubtitle = "§7Viel Spaß auf NexusLobby"; + + private File configFile; + private FileConfiguration config; + + // --- Einstellungen --- + private final int TICKS_FLUG = 70; + private final int TICKS_PAUSE = 30; + + private final int TITLE_FADE_IN = 10; + private final int TITLE_STAY = 50; + private final int TITLE_FADE_OUT = 10; + + @Override + public String getName() { return "Intro"; } + + @Override + public void onEnable() { + Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); + if (NexusLobby.getInstance().getCommand("intro") != null) { + NexusLobby.getInstance().getCommand("intro").setExecutor(this); + } + loadConfig(); + } + + @Override + public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); + activeIntro.clear(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Persistenz + // ───────────────────────────────────────────────────────────────────────── + + private void loadConfig() { + points.clear(); + labels.clear(); + configFile = new File(NexusLobby.getInstance().getDataFolder(), "intro.yml"); + if (!configFile.exists()) { + try { configFile.createNewFile(); } catch (IOException ignored) {} + } + config = YamlConfiguration.loadConfiguration(configFile); + + // Abschluss-Titel laden + endTitle = config.getString("end-title", "§a§lWillkommen!"); + endSubtitle = config.getString("end-subtitle", "§7Viel Spaß auf NexusLobby"); + + // Punkte laden + List list = config.getList("intro-points"); + if (list != null) { + for (Object obj : list) { + if (obj instanceof Map map) { + Object locObj = map.get("location"); + Object labelObj = map.get("label"); + if (locObj instanceof Location loc) { + points.add(loc); + labels.add(labelObj != null ? labelObj.toString() : ""); + } + } + } + } + + // Rückwärts-Kompatibilität + if (points.isEmpty()) { + List oldList = config.getList("points"); + if (oldList != null) { + for (Object obj : oldList) { + if (obj instanceof Location loc) { + points.add(loc); + labels.add(""); + } + } + } + } + } + + private void saveConfig() { + // Abschluss-Titel speichern + config.set("end-title", endTitle); + config.set("end-subtitle", endSubtitle); + + // Punkte speichern + List> data = new ArrayList<>(); + for (int i = 0; i < points.size(); i++) { + Map entry = new LinkedHashMap<>(); + entry.put("location", points.get(i)); + entry.put("label", labels.get(i)); + data.add(entry); + } + config.set("intro-points", data); + config.set("points", null); + + try { + config.save(configFile); + } catch (IOException e) { + NexusLobby.getInstance().getLogger().severe("Fehler beim Speichern der Intro-Config: " + e.getMessage()); + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Events + // ───────────────────────────────────────────────────────────────────────── + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + if (!event.getPlayer().hasPlayedBefore() && points.size() >= 2) { + startIntro(event.getPlayer()); + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Befehle + // ───────────────────────────────────────────────────────────────────────── + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(sender instanceof Player p)) return true; + if (!p.hasPermission("nexuslobby.admin")) return true; + + if (args.length == 0) { + sendHelp(p); + return true; + } + + switch (args[0].toLowerCase()) { + + case "add" -> { + String pointLabel = args.length > 1 + ? String.join(" ", Arrays.copyOfRange(args, 1, args.length)) + : ""; + points.add(p.getLocation()); + labels.add(pointLabel); + saveConfig(); + if (pointLabel.isEmpty()) { + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aPunkt §e#" + points.size() + " §awurde gesetzt."); + } else { + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aPunkt §e#" + points.size() + " §amit Text §e\"" + pointLabel + "\" §agesetzt."); + } + } + + case "clear" -> { + points.clear(); + labels.clear(); + saveConfig(); + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cAlle Intro-Punkte wurden gelöscht."); + } + + case "list" -> { + if (points.isEmpty()) { + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cKeine Punkte vorhanden."); + return true; + } + p.sendMessage("§8§m------------------------------------"); + p.sendMessage("§6§lIntro-Punkte (" + points.size() + ")"); + for (int i = 0; i < points.size(); i++) { + Location loc = points.get(i); + String lbl = labels.get(i).isEmpty() ? "§7(kein Text)" : "§e" + labels.get(i); + p.sendMessage("§8#" + (i + 1) + " §7" + + String.format("%.1f", loc.getX()) + ", " + + String.format("%.1f", loc.getY()) + ", " + + String.format("%.1f", loc.getZ()) + + " " + lbl); + } + p.sendMessage("§8§m------------------------------------"); + } + + case "start" -> { + if (points.size() < 2) { + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cDu brauchst mindestens 2 Punkte für eine Fahrt."); + } else { + startIntro(p); + } + } + + // /intro setendtitle — setzt den großen Abschluss-Titel + case "setendtitle" -> { + if (args.length < 2) { + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cBenutzung: §e/intro setendtitle "); + p.sendMessage("§7Farbcodes mit §e& §7möglich. Beispiel: §e/intro setendtitle &a&lWillkommen!"); + return true; + } + endTitle = colorize(String.join(" ", Arrays.copyOfRange(args, 1, args.length))); + saveConfig(); + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aAbschluss-Titel gesetzt: " + endTitle); + // Vorschau + p.sendTitle(endTitle, endSubtitle, TITLE_FADE_IN, TITLE_STAY, TITLE_FADE_OUT); + } + + // /intro setendsubtitle — setzt den kleinen Abschluss-Untertitel + case "setendsubtitle" -> { + if (args.length < 2) { + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cBenutzung: §e/intro setendsubtitle "); + p.sendMessage("§7Farbcodes mit §e& §7möglich. Beispiel: §e/intro setendsubtitle &7Viel Spaß!"); + return true; + } + endSubtitle = colorize(String.join(" ", Arrays.copyOfRange(args, 1, args.length))); + saveConfig(); + p.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aAbschluss-Untertitel gesetzt: " + endSubtitle); + // Vorschau + p.sendTitle(endTitle, endSubtitle, TITLE_FADE_IN, TITLE_STAY, TITLE_FADE_OUT); + } + + default -> sendHelp(p); + } + return true; + } + + private void sendHelp(Player p) { + p.sendMessage("§8§m------------------------------------"); + p.sendMessage("§6§lNexus Intro System (Cinematic)"); + p.sendMessage("§e/intro add [Text] §7- Punkt + optionaler Titel"); + p.sendMessage("§e/intro clear §7- Alle Punkte löschen"); + p.sendMessage("§e/intro list §7- Alle Punkte anzeigen"); + p.sendMessage("§e/intro start §7- Fahrt testen"); + p.sendMessage("§e/intro setendtitle §7- Abschluss-Titel setzen"); + p.sendMessage("§e/intro setendsubtitle §7- Abschluss-Untertitel setzen"); + p.sendMessage("§7Aktuell: §r" + endTitle + " §8/ §r" + endSubtitle); + p.sendMessage("§8§m------------------------------------"); + } + + // ───────────────────────────────────────────────────────────────────────── + // Intro-Logik + // ───────────────────────────────────────────────────────────────────────── + + public void startIntro(Player player) { + activeIntro.add(player.getUniqueId()); + player.setGameMode(GameMode.SPECTATOR); + + // Titel für den ersten Punkt sofort anzeigen + showTitleForSegment(player, 0); + + new BukkitRunnable() { + int currentSegment = 0; + int tickInSegment = 0; + boolean isPausing = true; + int lastTitleSegment = 0; + + @Override + public void run() { + try { + if (!player.isOnline() || !activeIntro.contains(player.getUniqueId())) { + this.cancel(); + return; + } + + if (currentSegment >= points.size() - 1) { + stopIntro(player); + this.cancel(); + return; + } + + Location start = points.get(currentSegment); + Location end = points.get(currentSegment + 1); + + if (isPausing) { + player.teleport(start); + tickInSegment++; + if (tickInSegment >= TICKS_PAUSE) { + isPausing = false; + tickInSegment = 0; + } + } else { + double progress = (double) tickInSegment / (double) TICKS_FLUG; + double smoothT = progress * progress * (3 - 2 * progress); + + player.teleport(interpolate(start, end, smoothT)); + + tickInSegment++; + if (tickInSegment >= TICKS_FLUG) { + isPausing = true; + tickInSegment = 0; + currentSegment++; + + if (currentSegment < points.size() && currentSegment != lastTitleSegment) { + showTitleForSegment(player, currentSegment); + lastTitleSegment = currentSegment; + } + } + } + + } catch (Exception e) { + this.cancel(); + stopIntro(player); + } + } + }.runTaskTimer(NexusLobby.getInstance(), 0L, 1L); + } + + private void showTitleForSegment(Player player, int segmentIndex) { + if (segmentIndex < 0 || segmentIndex >= labels.size()) return; + String lbl = labels.get(segmentIndex); + if (lbl == null || lbl.isEmpty()) return; + + player.sendTitle( + "§6§l" + lbl, + "§7✦ NexusLobby ✦", + TITLE_FADE_IN, + TITLE_STAY, + TITLE_FADE_OUT + ); + } + + private Location interpolate(Location start, Location end, double t) { + double x = start.getX() + (end.getX() - start.getX()) * t; + double y = start.getY() + (end.getY() - start.getY()) * t; + double z = start.getZ() + (end.getZ() - start.getZ()) * t; + + float startYaw = start.getYaw(); + float endYaw = end.getYaw(); + float diff = (endYaw - startYaw) % 360; + if (diff > 180) diff -= 360; + if (diff < -180) diff += 360; + float yaw = startYaw + diff * (float) t; + float pitch = (float) (start.getPitch() + (end.getPitch() - start.getPitch()) * t); + + return new Location(start.getWorld(), x, y, z, yaw, pitch); + } + + private void stopIntro(Player player) { + activeIntro.remove(player.getUniqueId()); + player.setGameMode(GameMode.ADVENTURE); + player.teleport(player.getWorld().getSpawnLocation()); + + // Abschluss-Titel — komplett konfigurierbar + player.sendTitle(endTitle, endSubtitle, 10, 60, 20); + } + + /** Ersetzt &-Farbcodes durch §-Codes */ + private String colorize(String input) { + return input.replace("&", "§"); + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/mapart/MapArtModule.java b/src/main/java/de/nexuslobby/modules/mapart/MapArtModule.java index d1f8202..5a9aa44 100644 --- a/src/main/java/de/nexuslobby/modules/mapart/MapArtModule.java +++ b/src/main/java/de/nexuslobby/modules/mapart/MapArtModule.java @@ -1,310 +1,310 @@ -package de.nexuslobby.modules.mapart; - -import de.nexuslobby.NexusLobby; -import de.nexuslobby.api.Module; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.ItemFrame; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.MapMeta; -import org.bukkit.map.MapCanvas; -import org.bukkit.map.MapRenderer; -import org.bukkit.map.MapView; -import org.bukkit.util.RayTraceResult; -import org.jetbrains.annotations.NotNull; - -import javax.imageio.ImageIO; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; - -public class MapArtModule implements Module, CommandExecutor { - - private File storageFile; - private FileConfiguration storageConfig; - - // FIX: Unbegrenzter Cache führt bei vielen verschiedenen Bild-URLs zu einem - // OutOfMemoryError. Stattdessen nutzen wir einen LRU-Cache mit max. 50 Einträgen. - // Älteste Kacheln werden automatisch verdrängt. - private static final int MAX_CACHE_SIZE = 50; - private final Map tileCache = Collections.synchronizedMap( - new LinkedHashMap(MAX_CACHE_SIZE + 1, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > MAX_CACHE_SIZE; - } - } - ); - // Hilfs-Set um parallele Downloads der gleichen URL zu verhindern - private final Map loadingShield = new ConcurrentHashMap<>(); - - @Override - public String getName() { return "MapArt"; } - - @Override - public void onEnable() { - if (NexusLobby.getInstance().getCommand("mapart") != null) { - NexusLobby.getInstance().getCommand("mapart").setExecutor(this); - } - initStorage(); - reloadMaps(); - } - - @Override - public void onDisable() { - saveStorage(); - tileCache.clear(); - } - - private void initStorage() { - storageFile = new File(NexusLobby.getInstance().getDataFolder(), "mapart.yml"); - if (!storageFile.exists()) { - try { - storageFile.createNewFile(); - } catch (IOException e) { - NexusLobby.getInstance().getLogger().severe("Fehler beim Erstellen der mapart.yml: " + e.getMessage()); - } - } - storageConfig = YamlConfiguration.loadConfiguration(storageFile); - } - - private void saveStorage() { - try { - storageConfig.save(storageFile); - } catch (IOException e) { - NexusLobby.getInstance().getLogger().severe("Fehler beim Speichern der mapart.yml: " + e.getMessage()); - } - } - - private void reloadMaps() { - ConfigurationSection section = storageConfig.getConfigurationSection("active_maps"); - if (section == null) return; - - for (String idStr : section.getKeys(false)) { - try { - int mapId = Integer.parseInt(idStr); - String url = section.getString(idStr + ".url"); - int x = section.getInt(idStr + ".x"); - int y = section.getInt(idStr + ".y"); - int totalW = section.getInt(idStr + ".totalW"); - int totalH = section.getInt(idStr + ".totalH"); - - @SuppressWarnings("deprecation") - MapView view = Bukkit.getMap(mapId); - if (view != null) { - applyRenderer(view, url, x, y, totalW, totalH); - } - } catch (Exception ignored) {} - } - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { - if (!(sender instanceof Player player)) return true; - if (!player.hasPermission("nexuslobby.mapart")) { - player.sendMessage("§8[§6Nexus§8] §cKeine Rechte."); - return true; - } - - if (args.length == 0) { - sendHelp(player); - return true; - } - - if (args[0].equalsIgnoreCase("delete")) { - int radius = 3; - if (args.length == 2) { - try { - radius = Integer.parseInt(args[1]); - } catch (NumberFormatException e) { - player.sendMessage("§8[§6Nexus§8] §cUngültiger Radius."); - return true; - } - } - deleteNearbyMaps(player, radius); - return true; - } - - if (args.length != 2) { - sendHelp(player); - return true; - } - - RayTraceResult rayTrace = player.rayTraceBlocks(5); - if (rayTrace == null || rayTrace.getHitBlock() == null || rayTrace.getHitBlockFace() == null) { - player.sendMessage("§8[§6Nexus§8] §cBitte schaue eine Wand direkt an."); - return true; - } - - Block targetBlock = rayTrace.getHitBlock(); - BlockFace hitFace = rayTrace.getHitBlockFace(); - - if (hitFace == BlockFace.UP || hitFace == BlockFace.DOWN) { - player.sendMessage("§8[§6Nexus§8] §cAktuell nur an vertikalen Wänden möglich."); - return true; - } - - String url = args[0]; - int width, height; - try { - String[] parts = args[1].toLowerCase().split("x"); - width = Integer.parseInt(parts[0]); - height = Integer.parseInt(parts[1]); - } catch (Exception e) { - player.sendMessage("§8[§6Nexus§8] §cUngültiges Format (z.B. 6x4)."); - return true; - } - - player.sendMessage("§8[§6Nexus§8] §7Bild wird verarbeitet..."); - createRaster(player, targetBlock, hitFace, url, width, height); - - return true; - } - - private void sendHelp(Player p) { - p.sendMessage("§8§m------------------------------------"); - p.sendMessage("§6§lNexus MapArt"); - p.sendMessage("§e/mapart §7- Bild erstellen"); - p.sendMessage("§e/mapart delete [Radius] §7- Bilder in der Nähe löschen"); - p.sendMessage("§8§m------------------------------------"); - } - - private void deleteNearbyMaps(Player player, int radius) { - AtomicInteger count = new AtomicInteger(0); - player.getNearbyEntities(radius, radius, radius).forEach(entity -> { - if (entity instanceof ItemFrame frame) { - ItemStack item = frame.getItem(); - if (item != null && item.getType() == Material.FILLED_MAP && item.getItemMeta() instanceof MapMeta meta) { - if (meta.hasMapView()) { - int id = meta.getMapView().getId(); - if (storageConfig.contains("active_maps." + id)) { - storageConfig.set("active_maps." + id, null); - frame.remove(); - count.incrementAndGet(); - } - } - } - } - }); - saveStorage(); - player.sendMessage("§8[§6Nexus§8] §aErfolgreich §e" + count.get() + " §aKartenelemente gelöscht."); - } - - private void createRaster(Player player, Block startBlock, BlockFace face, String url, int gridW, int gridH) { - BlockFace rightDirection; - switch (face) { - case NORTH: rightDirection = BlockFace.WEST; break; - case SOUTH: rightDirection = BlockFace.EAST; break; - case EAST: rightDirection = BlockFace.NORTH; break; - case WEST: rightDirection = BlockFace.SOUTH; break; - default: rightDirection = BlockFace.EAST; - } - - Block origin = startBlock.getRelative(face); - - for (int y = 0; y < gridH; y++) { - for (int x = 0; x < gridW; x++) { - Block currentPos = origin.getRelative(rightDirection, x).getRelative(BlockFace.DOWN, y); - spawnPersistentFrame(player, currentPos, face, url, x, y, gridW, gridH); - } - } - player.sendMessage("§8[§6Nexus§8] §aBild-Raster wurde permanent platziert!"); - } - - private void spawnPersistentFrame(Player player, Block pos, BlockFace face, String url, int x, int y, int totalW, int totalH) { - MapView view = Bukkit.createMap(player.getWorld()); - applyRenderer(view, url, x, y, totalW, totalH); - - String path = "active_maps." + view.getId(); - storageConfig.set(path + ".url", url); - storageConfig.set(path + ".x", x); - storageConfig.set(path + ".y", y); - storageConfig.set(path + ".totalW", totalW); - storageConfig.set(path + ".totalH", totalH); - saveStorage(); - - ItemStack mapStack = new ItemStack(Material.FILLED_MAP); - MapMeta meta = (MapMeta) mapStack.getItemMeta(); - if (meta != null) { - meta.setMapView(view); - mapStack.setItemMeta(meta); - } - - try { - ItemFrame frame = player.getWorld().spawn(pos.getLocation(), ItemFrame.class); - frame.setFacingDirection(face); - frame.setItem(mapStack); - frame.setVisible(false); - frame.setFixed(true); - } catch (Exception ignored) {} - } - - private void applyRenderer(MapView view, String url, int tileX, int tileY, int totalW, int totalH) { - view.getRenderers().forEach(view::removeRenderer); - view.addRenderer(new MapRenderer() { - private final String cacheKey = url + "_" + totalW + "x" + totalH + "_" + tileX + "_" + tileY; - private boolean errorLogged = false; - - @Override - public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player p) { - // 1. Wenn Kachel im Cache, sofort zeichnen (extrem schnell) - if (tileCache.containsKey(cacheKey)) { - canvas.drawImage(0, 0, tileCache.get(cacheKey)); - return; - } - - // 2. Wenn bereits ein Thread für diese URL lädt, abbrechen (verhindert Spam) - if (loadingShield.containsKey(url)) return; - - // 3. Bild asynchron laden - loadingShield.put(url, true); - Bukkit.getScheduler().runTaskAsynchronously(NexusLobby.getInstance(), () -> { - try { - BufferedImage original = ImageIO.read(new URL(url)); - if (original != null) { - int targetW = totalW * 128; - int targetH = totalH * 128; - - BufferedImage fullScaled = new BufferedImage(targetW, targetH, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = fullScaled.createGraphics(); - g.drawImage(original.getScaledInstance(targetW, targetH, Image.SCALE_SMOOTH), 0, 0, null); - g.dispose(); - - // Alle benötigten Kacheln für dieses Bild in den Cache legen - for (int ty = 0; ty < totalH; ty++) { - for (int tx = 0; tx < totalW; tx++) { - String key = url + "_" + totalW + "x" + totalH + "_" + tx + "_" + ty; - tileCache.put(key, fullScaled.getSubimage(tx * 128, ty * 128, 128, 128)); - } - } - } - } catch (Exception e) { - if (!errorLogged) { - NexusLobby.getInstance().getLogger().warning("Fehler beim Laden von MapArt: " + url); - errorLogged = true; - } - } finally { - loadingShield.remove(url); - } - }); - } - }); - } +package de.nexuslobby.modules.mapart; + +import de.nexuslobby.NexusLobby; +import de.nexuslobby.api.Module; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.ItemFrame; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.MapMeta; +import org.bukkit.map.MapCanvas; +import org.bukkit.map.MapRenderer; +import org.bukkit.map.MapView; +import org.bukkit.util.RayTraceResult; +import org.jetbrains.annotations.NotNull; + +import javax.imageio.ImageIO; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class MapArtModule implements Module, CommandExecutor { + + private File storageFile; + private FileConfiguration storageConfig; + + // FIX: Unbegrenzter Cache führt bei vielen verschiedenen Bild-URLs zu einem + // OutOfMemoryError. Stattdessen nutzen wir einen LRU-Cache mit max. 50 Einträgen. + // Älteste Kacheln werden automatisch verdrängt. + private static final int MAX_CACHE_SIZE = 50; + private final Map tileCache = Collections.synchronizedMap( + new LinkedHashMap(MAX_CACHE_SIZE + 1, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_CACHE_SIZE; + } + } + ); + // Hilfs-Set um parallele Downloads der gleichen URL zu verhindern + private final Map loadingShield = new ConcurrentHashMap<>(); + + @Override + public String getName() { return "MapArt"; } + + @Override + public void onEnable() { + if (NexusLobby.getInstance().getCommand("mapart") != null) { + NexusLobby.getInstance().getCommand("mapart").setExecutor(this); + } + initStorage(); + reloadMaps(); + } + + @Override + public void onDisable() { + saveStorage(); + tileCache.clear(); + } + + private void initStorage() { + storageFile = new File(NexusLobby.getInstance().getDataFolder(), "mapart.yml"); + if (!storageFile.exists()) { + try { + storageFile.createNewFile(); + } catch (IOException e) { + NexusLobby.getInstance().getLogger().severe("Fehler beim Erstellen der mapart.yml: " + e.getMessage()); + } + } + storageConfig = YamlConfiguration.loadConfiguration(storageFile); + } + + private void saveStorage() { + try { + storageConfig.save(storageFile); + } catch (IOException e) { + NexusLobby.getInstance().getLogger().severe("Fehler beim Speichern der mapart.yml: " + e.getMessage()); + } + } + + private void reloadMaps() { + ConfigurationSection section = storageConfig.getConfigurationSection("active_maps"); + if (section == null) return; + + for (String idStr : section.getKeys(false)) { + try { + int mapId = Integer.parseInt(idStr); + String url = section.getString(idStr + ".url"); + int x = section.getInt(idStr + ".x"); + int y = section.getInt(idStr + ".y"); + int totalW = section.getInt(idStr + ".totalW"); + int totalH = section.getInt(idStr + ".totalH"); + + @SuppressWarnings("deprecation") + MapView view = Bukkit.getMap(mapId); + if (view != null) { + applyRenderer(view, url, x, y, totalW, totalH); + } + } catch (Exception ignored) {} + } + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(sender instanceof Player player)) return true; + if (!player.hasPermission("nexuslobby.mapart")) { + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cKeine Rechte."); + return true; + } + + if (args.length == 0) { + sendHelp(player); + return true; + } + + if (args[0].equalsIgnoreCase("delete")) { + int radius = 3; + if (args.length == 2) { + try { + radius = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cUngültiger Radius."); + return true; + } + } + deleteNearbyMaps(player, radius); + return true; + } + + if (args.length != 2) { + sendHelp(player); + return true; + } + + RayTraceResult rayTrace = player.rayTraceBlocks(5); + if (rayTrace == null || rayTrace.getHitBlock() == null || rayTrace.getHitBlockFace() == null) { + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cBitte schaue eine Wand direkt an."); + return true; + } + + Block targetBlock = rayTrace.getHitBlock(); + BlockFace hitFace = rayTrace.getHitBlockFace(); + + if (hitFace == BlockFace.UP || hitFace == BlockFace.DOWN) { + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cAktuell nur an vertikalen Wänden möglich."); + return true; + } + + String url = args[0]; + int width, height; + try { + String[] parts = args[1].toLowerCase().split("x"); + width = Integer.parseInt(parts[0]); + height = Integer.parseInt(parts[1]); + } catch (Exception e) { + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§cUngültiges Format (z.B. 6x4)."); + return true; + } + + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§7Bild wird verarbeitet..."); + createRaster(player, targetBlock, hitFace, url, width, height); + + return true; + } + + private void sendHelp(Player p) { + p.sendMessage("§8§m------------------------------------"); + p.sendMessage("§6§lNexus MapArt"); + p.sendMessage("§e/mapart §7- Bild erstellen"); + p.sendMessage("§e/mapart delete [Radius] §7- Bilder in der Nähe löschen"); + p.sendMessage("§8§m------------------------------------"); + } + + private void deleteNearbyMaps(Player player, int radius) { + AtomicInteger count = new AtomicInteger(0); + player.getNearbyEntities(radius, radius, radius).forEach(entity -> { + if (entity instanceof ItemFrame frame) { + ItemStack item = frame.getItem(); + if (item != null && item.getType() == Material.FILLED_MAP && item.getItemMeta() instanceof MapMeta meta) { + if (meta.hasMapView()) { + int id = meta.getMapView().getId(); + if (storageConfig.contains("active_maps." + id)) { + storageConfig.set("active_maps." + id, null); + frame.remove(); + count.incrementAndGet(); + } + } + } + } + }); + saveStorage(); + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aErfolgreich §e" + count.get() + " §aKartenelemente gelöscht."); + } + + private void createRaster(Player player, Block startBlock, BlockFace face, String url, int gridW, int gridH) { + BlockFace rightDirection; + switch (face) { + case NORTH: rightDirection = BlockFace.WEST; break; + case SOUTH: rightDirection = BlockFace.EAST; break; + case EAST: rightDirection = BlockFace.NORTH; break; + case WEST: rightDirection = BlockFace.SOUTH; break; + default: rightDirection = BlockFace.EAST; + } + + Block origin = startBlock.getRelative(face); + + for (int y = 0; y < gridH; y++) { + for (int x = 0; x < gridW; x++) { + Block currentPos = origin.getRelative(rightDirection, x).getRelative(BlockFace.DOWN, y); + spawnPersistentFrame(player, currentPos, face, url, x, y, gridW, gridH); + } + } + player.sendMessage(de.nexuslobby.utils.LangManager.getPrefix() + "§aBild-Raster wurde permanent platziert!"); + } + + private void spawnPersistentFrame(Player player, Block pos, BlockFace face, String url, int x, int y, int totalW, int totalH) { + MapView view = Bukkit.createMap(player.getWorld()); + applyRenderer(view, url, x, y, totalW, totalH); + + String path = "active_maps." + view.getId(); + storageConfig.set(path + ".url", url); + storageConfig.set(path + ".x", x); + storageConfig.set(path + ".y", y); + storageConfig.set(path + ".totalW", totalW); + storageConfig.set(path + ".totalH", totalH); + saveStorage(); + + ItemStack mapStack = new ItemStack(Material.FILLED_MAP); + MapMeta meta = (MapMeta) mapStack.getItemMeta(); + if (meta != null) { + meta.setMapView(view); + mapStack.setItemMeta(meta); + } + + try { + ItemFrame frame = player.getWorld().spawn(pos.getLocation(), ItemFrame.class); + frame.setFacingDirection(face); + frame.setItem(mapStack); + frame.setVisible(false); + frame.setFixed(true); + } catch (Exception ignored) {} + } + + private void applyRenderer(MapView view, String url, int tileX, int tileY, int totalW, int totalH) { + view.getRenderers().forEach(view::removeRenderer); + view.addRenderer(new MapRenderer() { + private final String cacheKey = url + "_" + totalW + "x" + totalH + "_" + tileX + "_" + tileY; + private boolean errorLogged = false; + + @Override + public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player p) { + // 1. Wenn Kachel im Cache, sofort zeichnen (extrem schnell) + if (tileCache.containsKey(cacheKey)) { + canvas.drawImage(0, 0, tileCache.get(cacheKey)); + return; + } + + // 2. Wenn bereits ein Thread für diese URL lädt, abbrechen (verhindert Spam) + if (loadingShield.containsKey(url)) return; + + // 3. Bild asynchron laden + loadingShield.put(url, true); + Bukkit.getScheduler().runTaskAsynchronously(NexusLobby.getInstance(), () -> { + try { + BufferedImage original = ImageIO.read(new URL(url)); + if (original != null) { + int targetW = totalW * 128; + int targetH = totalH * 128; + + BufferedImage fullScaled = new BufferedImage(targetW, targetH, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = fullScaled.createGraphics(); + g.drawImage(original.getScaledInstance(targetW, targetH, Image.SCALE_SMOOTH), 0, 0, null); + g.dispose(); + + // Alle benötigten Kacheln für dieses Bild in den Cache legen + for (int ty = 0; ty < totalH; ty++) { + for (int tx = 0; tx < totalW; tx++) { + String key = url + "_" + totalW + "x" + totalH + "_" + tx + "_" + ty; + tileCache.put(key, fullScaled.getSubimage(tx * 128, ty * 128, 128, 128)); + } + } + } + } catch (Exception e) { + if (!errorLogged) { + NexusLobby.getInstance().getLogger().warning("Fehler beim Laden von MapArt: " + url); + errorLogged = true; + } + } finally { + loadingShield.remove(url); + } + }); + } + }); + } } \ 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 index cafe5cd..eac853a 100644 --- a/src/main/java/de/nexuslobby/modules/parkour/ParkourListener.java +++ b/src/main/java/de/nexuslobby/modules/parkour/ParkourListener.java @@ -1,107 +1,107 @@ -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; - } - - @EventHandler - public void onInteract(PlayerInteractAtEntityEvent event) { - if (!(event.getRightClicked() instanceof ArmorStand as)) return; - if (!as.getScoreboardTags().contains(NPC_TAG)) return; - - Player player = event.getPlayer(); - event.setCancelled(true); - - 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; - } - } - - manager.startParkour(player, as.getLocation()); - // Strecken-Nummer dem Spieler mitteilen - int track = manager.getActiveTrack(player); - if (track != -1) { - player.sendMessage("§8[§6Parkour§8] §eViel Erfolg auf §bStrecke " + track + "§e! Erreiche das Ziel so schnell wie möglich."); - } - player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1f); - } - - @EventHandler - public void onMove(PlayerMoveEvent event) { - // Performance: Nur bei Block-Wechsel berechnen - 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 --- - // Absturz-Y aus config lesen (Standard: 50), damit Maps unter Y=50 korrekt funktionieren - double fallY = NexusLobby.getInstance().getConfig().getDouble("parkour.fall_y", 50.0); - if (loc.getY() < fallY) { - 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 (Track-spezifisch) --- - List cps = manager.getOrderedCheckpoints(player); - for (int i = 0; i < cps.size(); i++) { - if (isNearby(loc, cps.get(i))) { - manager.reachCheckpoint(player, i); - } - } - - // --- 3. ZIEL-CHECK (Track-spezifisch) --- - Location finish = manager.getFinishLocation(player); - if (finish != null && isNearby(loc, finish)) { - manager.finishParkour(player); - startCooldown.put(player.getUniqueId(), System.currentTimeMillis()); - } - } - - private boolean isNearby(Location playerLoc, Location targetLoc) { - if (playerLoc.getWorld() == null || !playerLoc.getWorld().equals(targetLoc.getWorld())) return false; - return playerLoc.distanceSquared(targetLoc) <= 2.25; // ~1.5 Blöcke Radius - } - - @EventHandler - public void onQuit(PlayerQuitEvent event) { - UUID uuid = event.getPlayer().getUniqueId(); - manager.stopParkour(event.getPlayer()); - startCooldown.remove(uuid); - } +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; + } + + @EventHandler + public void onInteract(PlayerInteractAtEntityEvent event) { + if (!(event.getRightClicked() instanceof ArmorStand as)) return; + if (!as.getScoreboardTags().contains(NPC_TAG)) return; + + Player player = event.getPlayer(); + event.setCancelled(true); + + 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; + } + } + + manager.startParkour(player, as.getLocation()); + // Strecken-Nummer dem Spieler mitteilen + int track = manager.getActiveTrack(player); + if (track != -1) { + player.sendMessage("§8[§6Parkour§8] §eViel Erfolg auf §bStrecke " + track + "§e! Erreiche das Ziel so schnell wie möglich."); + } + player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1f); + } + + @EventHandler + public void onMove(PlayerMoveEvent event) { + // Performance: Nur bei Block-Wechsel berechnen + 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 --- + // Absturz-Y aus config lesen (Standard: 50), damit Maps unter Y=50 korrekt funktionieren + double fallY = NexusLobby.getInstance().getConfig().getDouble("parkour.fall_y", 50.0); + if (loc.getY() < fallY) { + 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 (Track-spezifisch) --- + List cps = manager.getOrderedCheckpoints(player); + for (int i = 0; i < cps.size(); i++) { + if (isNearby(loc, cps.get(i))) { + manager.reachCheckpoint(player, i); + } + } + + // --- 3. ZIEL-CHECK (Track-spezifisch) --- + Location finish = manager.getFinishLocation(player); + if (finish != null && isNearby(loc, finish)) { + manager.finishParkour(player); + startCooldown.put(player.getUniqueId(), System.currentTimeMillis()); + } + } + + private boolean isNearby(Location playerLoc, Location targetLoc) { + if (playerLoc.getWorld() == null || !playerLoc.getWorld().equals(targetLoc.getWorld())) return false; + return playerLoc.distanceSquared(targetLoc) <= 2.25; // ~1.5 Blöcke Radius + } + + @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 index e3584fe..ca89433 100644 --- a/src/main/java/de/nexuslobby/modules/parkour/ParkourManager.java +++ b/src/main/java/de/nexuslobby/modules/parkour/ParkourManager.java @@ -1,464 +1,464 @@ -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; - -/** - * ParkourManager – unterstützt zwei Strecken (Track 1 & 2). - * - * Config-Struktur (parkour.yml): - * tracks: - * 1: - * start: - * finish: - * checkpoints: - * 1: - * 2: - * ... - * 2: - * start: - * finish: - * checkpoints: - * 1: - * ... - * besttimes: - * : - * names: - * : - * - * Beim Start wird zufällig eine der beiden konfigurierten Strecken ausgewählt. - * Beide Strecken müssen die gleiche Anzahl an Checkpoints haben – andernfalls - * wird nur die vollständige gewählt. - */ -public class ParkourManager { - - private final NexusLobby plugin; - private final File file; - private FileConfiguration config; - - // Aktive Läufe - private final Map startTime = new HashMap<>(); - private final Map lastCheckpointLoc = new HashMap<>(); - private final Map nextCheckpointIndex= new HashMap<>(); - /** Welche Track-Nummer (1 oder 2) der Spieler gerade läuft */ - private final Map activeTrack = new HashMap<>(); - - private final Random random = new Random(); - - public ParkourManager(NexusLobby plugin) { - this.plugin = plugin; - this.file = new File(plugin.getDataFolder(), "parkour.yml"); - loadConfig(); - migrateLegacyBestTimes(); - startParticleTask(); - } - - // ───────────────────────────────────────────────────────────────────────── - // Config I/O - // ───────────────────────────────────────────────────────────────────────── - - private void loadConfig() { - if (!file.exists()) { - file.getParentFile().mkdirs(); - try { - file.createNewFile(); - } catch (IOException e) { - plugin.getLogger().severe("Fehler beim Erstellen der Parkour-Datei: " + e.getMessage()); - } - } - config = YamlConfiguration.loadConfiguration(file); - } - - /** - * Migriert Bestzeiten vom alten Format (besttimes.) ins neue Format - * (besttimes..). Alte Zeiten werden Track 1 zugeordnet, da vor - * dieser Änderung nur ein Track existierte. - * Läuft einmalig beim Start; tut nichts wenn keine alten Daten vorhanden sind. - */ - private void migrateLegacyBestTimes() { - if (!config.contains("besttimes")) return; - - // Prüfen ob es direkte UUID-Keys unter besttimes gibt (altes Format) - // Im neuen Format wären hier nur "1" und "2" als Keys - var section = config.getConfigurationSection("besttimes"); - if (section == null) return; - - int migrated = 0; - for (String key : new java.util.HashSet<>(section.getKeys(false))) { - // Neue Keys sind "1" oder "2" – alte Keys sind UUID-Strings (36 Zeichen, enthalten "-") - if (!key.equals("1") && !key.equals("2") && key.contains("-")) { - double time = config.getDouble("besttimes." + key); - // Nur migrieren wenn unter besttimes.1. noch kein besserer Wert steht - String newPath = "besttimes.1." + key; - double existingTime = config.getDouble(newPath, 99999.9); - if (time < existingTime) { - config.set(newPath, time); - } - // Alten Key entfernen - config.set("besttimes." + key, null); - migrated++; - } - } - - if (migrated > 0) { - save(); - plugin.getLogger().info("[Parkour] Migration: " + migrated - + " Bestzeit(en) von altem Format nach Strecke 1 übertragen."); - } - } - - private void save() { - try { - config.save(file); - } catch (IOException e) { - plugin.getLogger().severe("Fehler beim Speichern der Parkour-Config: " + e.getMessage()); - } - } - - // ───────────────────────────────────────────────────────────────────────── - // Setup-Befehle (Admin) - // ───────────────────────────────────────────────────────────────────────── - - public void setStartLocation(Player player, int track) { - config.set("tracks." + track + ".start", player.getLocation()); - save(); - player.sendMessage("§8[§6Parkour§8] §7Start von §eStrecke " + track + " §7an deiner Position gesetzt."); - } - - public void setFinishLocation(Player player, int track) { - config.set("tracks." + track + ".finish", player.getLocation()); - save(); - player.sendMessage("§8[§6Parkour§8] §7Ziel von §eStrecke " + track + " §7an deiner Position gesetzt."); - } - - public void setCheckpoint(Player player, int track) { - String cpBase = "tracks." + track + ".checkpoints"; - int nextId = 1; - if (config.contains(cpBase) && config.getConfigurationSection(cpBase) != null) { - nextId = config.getConfigurationSection(cpBase).getKeys(false).size() + 1; - } - config.set(cpBase + "." + nextId, player.getLocation()); - save(); - player.sendMessage("§8[§6Parkour§8] §7Checkpoint §e#" + nextId + " §7(Strecke " + track + ") §7gesetzt."); - player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1.0f, 1.5f); - } - - /** - * Löscht alle Punkte beider Strecken und bricht laufende Runs ab. - */ - public void removeAllPoints() { - config.set("tracks", null); - save(); - for (UUID uuid : new HashSet<>(startTime.keySet())) { - Player p = Bukkit.getPlayer(uuid); - if (p != null) { - stopParkour(p); - p.sendMessage("§8[§6Parkour§8] §cDie Strecken wurden soeben gelöscht."); - } - } - } - - // ───────────────────────────────────────────────────────────────────────── - // Getter (Track-spezifisch) - // ───────────────────────────────────────────────────────────────────────── - - public Location getStartLocation(int track) { - return config.getLocation("tracks." + track + ".start"); - } - - public Location getFinishLocation(int track) { - return config.getLocation("tracks." + track + ".finish"); - } - - public List getOrderedCheckpoints(int track) { - String cpBase = "tracks." + track + ".checkpoints"; - if (!config.contains(cpBase) || config.getConfigurationSection(cpBase) == null) - return new ArrayList<>(); - - return config.getConfigurationSection(cpBase).getKeys(false).stream() - .sorted(Comparator.comparingInt(Integer::parseInt)) - .map(key -> config.getLocation(cpBase + "." + key)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - /** - * Gibt die aktive Track-Nummer des Spielers zurück (1 oder 2), oder -1 wenn nicht aktiv. - */ - public int getActiveTrack(Player player) { - return activeTrack.getOrDefault(player.getUniqueId(), -1); - } - - // Convenience-Wrapper für ParkourListener (nutzt den aktiven Track des Spielers) - public List getOrderedCheckpoints(Player player) { - return getOrderedCheckpoints(getActiveTrack(player)); - } - - public Location getFinishLocation(Player player) { - return getFinishLocation(getActiveTrack(player)); - } - - // ───────────────────────────────────────────────────────────────────────── - // Parkour-Ablauf - // ───────────────────────────────────────────────────────────────────────── - - /** - * Startet den Parkour für den Spieler. - * Es wird zufällig eine der beiden Strecken gewählt, sofern beide vollständig - * konfiguriert sind (Start + Finish + mindestens gleiche Checkpoint-Anzahl). - * Falls nur eine Strecke konfiguriert ist, wird diese gewählt. - */ - public void startParkour(Player player, Location npcLocation) { - if (startTime.containsKey(player.getUniqueId())) return; - - int chosenTrack = pickRandomTrack(); - if (chosenTrack == -1) { - player.sendMessage("§8[§6Parkour§8] §cKeine Strecke konfiguriert. Bitte einen Admin kontaktieren."); - return; - } - - startTime.put(player.getUniqueId(), System.currentTimeMillis()); - lastCheckpointLoc.put(player.getUniqueId(), npcLocation); - nextCheckpointIndex.put(player.getUniqueId(), 0); - activeTrack.put(player.getUniqueId(), chosenTrack); - - 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); - } - - /** - * Wählt zufällig einen validen Track (1 oder 2). - * Bevorzugt Tracks mit gleichem Checkpoint-Count; fällt auf jeden konfigurierten zurück. - * Gibt -1 zurück, wenn kein Track konfiguriert ist. - */ - private int pickRandomTrack() { - boolean t1 = isTrackReady(1); - boolean t2 = isTrackReady(2); - - if (t1 && t2) { - // Beide vollständig – prüfen ob gleiche Länge - int len1 = getOrderedCheckpoints(1).size(); - int len2 = getOrderedCheckpoints(2).size(); - if (len1 != len2) { - // Warnung ins Log, aber trotzdem zufällig wählen - plugin.getLogger().warning("[Parkour] Strecke 1 hat " + len1 + " Checkpoints, Strecke 2 hat " + len2 + ". Bitte angleichen!"); - } - return random.nextBoolean() ? 1 : 2; - } else if (t1) { - return 1; - } else if (t2) { - return 2; - } - return -1; - } - - /** Gibt zurück, ob Start, Finish und mindestens 1 Checkpoint für den Track gesetzt sind. */ - public boolean isTrackReady(int track) { - return getStartLocation(track) != null - && getFinishLocation(track) != null - && !getOrderedCheckpoints(track).isEmpty(); - } - - private void startParticleTask() { - new BukkitRunnable() { - @Override - public void run() { - for (UUID uuid : startTime.keySet()) { - Player p = Bukkit.getPlayer(uuid); - if (p == null) continue; - - int track = activeTrack.getOrDefault(uuid, 1); - int nextIdx = nextCheckpointIndex.getOrDefault(uuid, 0); - List cps = getOrderedCheckpoints(track); - - 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(track); - 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) return; - - int track = activeTrack.getOrDefault(player.getUniqueId(), 1); - List cps = getOrderedCheckpoints(track); - if (reachedIndex >= cps.size()) return; - - 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; - - int track = activeTrack.getOrDefault(player.getUniqueId(), 1); - if (nextCheckpointIndex.getOrDefault(player.getUniqueId(), 0) < getOrderedCheckpoints(track).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! §8(Strecke " + track + ")"); - 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()); - activeTrack.remove(player.getUniqueId()); - } - - // ───────────────────────────────────────────────────────────────────────── - // Bestzeiten - // ───────────────────────────────────────────────────────────────────────── - - private void saveBestTime(Player player, double time) { - int track = activeTrack.getOrDefault(player.getUniqueId(), 1); - String path = "besttimes." + track + "." + 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."); - } - } - - /** - * Löscht NUR die Bestzeiten-Liste (besttimes + names). - * Die Strecken-Konfiguration (tracks) bleibt vollständig erhalten. - * Wird durch "/nexus parkour clear" aufgerufen. - */ - public void clearStats() { - config.set("besttimes", null); - config.set("names", null); - save(); - } - - public void clearStats(int track) { - config.set("besttimes." + track, null); - save(); - } - - public String getTopTen() { - // Fallback: kombiniert beide Tracks (Legacy-Support für %nexuslobby_parkour_top%) - return getTopTen(0); - } - - public String getTopTen(int track) { - String trackLabel = track == 1 ? " §8(Strecke 1)" : track == 2 ? " §8(Strecke 2)" : ""; - - Map allTimes = new HashMap<>(); - - if (track == 0) { - // Alle Tracks kombiniert (nimmt die beste Zeit je Spieler über alle Tracks) - for (int t = 1; t <= 2; t++) { - String section = "besttimes." + t; - if (config.contains(section) && config.getConfigurationSection(section) != null) { - for (String uuidStr : config.getConfigurationSection(section).getKeys(false)) { - double time = config.getDouble(section + "." + uuidStr); - allTimes.merge(uuidStr, time, Math::min); - } - } - } - } else { - String section = "besttimes." + track; - if (!config.contains(section) || config.getConfigurationSection(section) == null) { - return "§6§l🏆 TOP 10 PARKOUR" + trackLabel + "\n§7Noch keine Rekorde."; - } - for (String uuidStr : config.getConfigurationSection(section).getKeys(false)) { - allTimes.put(uuidStr, config.getDouble(section + "." + uuidStr)); - } - } - - if (allTimes.isEmpty()) - return "§6§l🏆 TOP 10 PARKOUR" + trackLabel + "\n§7Noch keine Rekorde."; - - List> sortedList = allTimes.entrySet().stream() - .sorted(Map.Entry.comparingByValue()) - .limit(10) - .toList(); - - StringBuilder builder = new StringBuilder("§6§l🏆 TOP 10 PARKOUR" + trackLabel); - 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(); - } - - // ───────────────────────────────────────────────────────────────────────── - // Hilfsmethoden - // ───────────────────────────────────────────────────────────────────────── - - public boolean isIngame(Player player) { return startTime.containsKey(player.getUniqueId()); } - public Location getCheckpoint(Player player) { return lastCheckpointLoc.get(player.getUniqueId()); } - - /** Gibt die Strecken-Info als lesbaren String aus (für Admin-Feedback). */ - public String getTrackInfo() { - StringBuilder sb = new StringBuilder("§8[§6Parkour§8] §7Track-Status:\n"); - for (int t = 1; t <= 2; t++) { - int cps = getOrderedCheckpoints(t).size(); - boolean ready = isTrackReady(t); - sb.append(" §eStrecke ").append(t).append(": ") - .append(ready ? "§a✔" : "§c✘") - .append(" §7(").append(cps).append(" Checkpoints)"); - if (t < 2) sb.append("\n"); - } - return sb.toString(); - } +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; + +/** + * ParkourManager – unterstützt zwei Strecken (Track 1 & 2). + * + * Config-Struktur (parkour.yml): + * tracks: + * 1: + * start: + * finish: + * checkpoints: + * 1: + * 2: + * ... + * 2: + * start: + * finish: + * checkpoints: + * 1: + * ... + * besttimes: + * : + * names: + * : + * + * Beim Start wird zufällig eine der beiden konfigurierten Strecken ausgewählt. + * Beide Strecken müssen die gleiche Anzahl an Checkpoints haben – andernfalls + * wird nur die vollständige gewählt. + */ +public class ParkourManager { + + private final NexusLobby plugin; + private final File file; + private FileConfiguration config; + + // Aktive Läufe + private final Map startTime = new HashMap<>(); + private final Map lastCheckpointLoc = new HashMap<>(); + private final Map nextCheckpointIndex= new HashMap<>(); + /** Welche Track-Nummer (1 oder 2) der Spieler gerade läuft */ + private final Map activeTrack = new HashMap<>(); + + private final Random random = new Random(); + + public ParkourManager(NexusLobby plugin) { + this.plugin = plugin; + this.file = new File(plugin.getDataFolder(), "parkour.yml"); + loadConfig(); + migrateLegacyBestTimes(); + startParticleTask(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Config I/O + // ───────────────────────────────────────────────────────────────────────── + + private void loadConfig() { + if (!file.exists()) { + file.getParentFile().mkdirs(); + try { + file.createNewFile(); + } catch (IOException e) { + plugin.getLogger().severe("Fehler beim Erstellen der Parkour-Datei: " + e.getMessage()); + } + } + config = YamlConfiguration.loadConfiguration(file); + } + + /** + * Migriert Bestzeiten vom alten Format (besttimes.) ins neue Format + * (besttimes..). Alte Zeiten werden Track 1 zugeordnet, da vor + * dieser Änderung nur ein Track existierte. + * Läuft einmalig beim Start; tut nichts wenn keine alten Daten vorhanden sind. + */ + private void migrateLegacyBestTimes() { + if (!config.contains("besttimes")) return; + + // Prüfen ob es direkte UUID-Keys unter besttimes gibt (altes Format) + // Im neuen Format wären hier nur "1" und "2" als Keys + var section = config.getConfigurationSection("besttimes"); + if (section == null) return; + + int migrated = 0; + for (String key : new java.util.HashSet<>(section.getKeys(false))) { + // Neue Keys sind "1" oder "2" – alte Keys sind UUID-Strings (36 Zeichen, enthalten "-") + if (!key.equals("1") && !key.equals("2") && key.contains("-")) { + double time = config.getDouble("besttimes." + key); + // Nur migrieren wenn unter besttimes.1. noch kein besserer Wert steht + String newPath = "besttimes.1." + key; + double existingTime = config.getDouble(newPath, 99999.9); + if (time < existingTime) { + config.set(newPath, time); + } + // Alten Key entfernen + config.set("besttimes." + key, null); + migrated++; + } + } + + if (migrated > 0) { + save(); + plugin.getLogger().info("[Parkour] Migration: " + migrated + + " Bestzeit(en) von altem Format nach Strecke 1 übertragen."); + } + } + + private void save() { + try { + config.save(file); + } catch (IOException e) { + plugin.getLogger().severe("Fehler beim Speichern der Parkour-Config: " + e.getMessage()); + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Setup-Befehle (Admin) + // ───────────────────────────────────────────────────────────────────────── + + public void setStartLocation(Player player, int track) { + config.set("tracks." + track + ".start", player.getLocation()); + save(); + player.sendMessage("§8[§6Parkour§8] §7Start von §eStrecke " + track + " §7an deiner Position gesetzt."); + } + + public void setFinishLocation(Player player, int track) { + config.set("tracks." + track + ".finish", player.getLocation()); + save(); + player.sendMessage("§8[§6Parkour§8] §7Ziel von §eStrecke " + track + " §7an deiner Position gesetzt."); + } + + public void setCheckpoint(Player player, int track) { + String cpBase = "tracks." + track + ".checkpoints"; + int nextId = 1; + if (config.contains(cpBase) && config.getConfigurationSection(cpBase) != null) { + nextId = config.getConfigurationSection(cpBase).getKeys(false).size() + 1; + } + config.set(cpBase + "." + nextId, player.getLocation()); + save(); + player.sendMessage("§8[§6Parkour§8] §7Checkpoint §e#" + nextId + " §7(Strecke " + track + ") §7gesetzt."); + player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1.0f, 1.5f); + } + + /** + * Löscht alle Punkte beider Strecken und bricht laufende Runs ab. + */ + public void removeAllPoints() { + config.set("tracks", null); + save(); + for (UUID uuid : new HashSet<>(startTime.keySet())) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { + stopParkour(p); + p.sendMessage("§8[§6Parkour§8] §cDie Strecken wurden soeben gelöscht."); + } + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Getter (Track-spezifisch) + // ───────────────────────────────────────────────────────────────────────── + + public Location getStartLocation(int track) { + return config.getLocation("tracks." + track + ".start"); + } + + public Location getFinishLocation(int track) { + return config.getLocation("tracks." + track + ".finish"); + } + + public List getOrderedCheckpoints(int track) { + String cpBase = "tracks." + track + ".checkpoints"; + if (!config.contains(cpBase) || config.getConfigurationSection(cpBase) == null) + return new ArrayList<>(); + + return config.getConfigurationSection(cpBase).getKeys(false).stream() + .sorted(Comparator.comparingInt(Integer::parseInt)) + .map(key -> config.getLocation(cpBase + "." + key)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Gibt die aktive Track-Nummer des Spielers zurück (1 oder 2), oder -1 wenn nicht aktiv. + */ + public int getActiveTrack(Player player) { + return activeTrack.getOrDefault(player.getUniqueId(), -1); + } + + // Convenience-Wrapper für ParkourListener (nutzt den aktiven Track des Spielers) + public List getOrderedCheckpoints(Player player) { + return getOrderedCheckpoints(getActiveTrack(player)); + } + + public Location getFinishLocation(Player player) { + return getFinishLocation(getActiveTrack(player)); + } + + // ───────────────────────────────────────────────────────────────────────── + // Parkour-Ablauf + // ───────────────────────────────────────────────────────────────────────── + + /** + * Startet den Parkour für den Spieler. + * Es wird zufällig eine der beiden Strecken gewählt, sofern beide vollständig + * konfiguriert sind (Start + Finish + mindestens gleiche Checkpoint-Anzahl). + * Falls nur eine Strecke konfiguriert ist, wird diese gewählt. + */ + public void startParkour(Player player, Location npcLocation) { + if (startTime.containsKey(player.getUniqueId())) return; + + int chosenTrack = pickRandomTrack(); + if (chosenTrack == -1) { + player.sendMessage("§8[§6Parkour§8] §cKeine Strecke konfiguriert. Bitte einen Admin kontaktieren."); + return; + } + + startTime.put(player.getUniqueId(), System.currentTimeMillis()); + lastCheckpointLoc.put(player.getUniqueId(), npcLocation); + nextCheckpointIndex.put(player.getUniqueId(), 0); + activeTrack.put(player.getUniqueId(), chosenTrack); + + 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); + } + + /** + * Wählt zufällig einen validen Track (1 oder 2). + * Bevorzugt Tracks mit gleichem Checkpoint-Count; fällt auf jeden konfigurierten zurück. + * Gibt -1 zurück, wenn kein Track konfiguriert ist. + */ + private int pickRandomTrack() { + boolean t1 = isTrackReady(1); + boolean t2 = isTrackReady(2); + + if (t1 && t2) { + // Beide vollständig – prüfen ob gleiche Länge + int len1 = getOrderedCheckpoints(1).size(); + int len2 = getOrderedCheckpoints(2).size(); + if (len1 != len2) { + // Warnung ins Log, aber trotzdem zufällig wählen + plugin.getLogger().warning("[Parkour] Strecke 1 hat " + len1 + " Checkpoints, Strecke 2 hat " + len2 + ". Bitte angleichen!"); + } + return random.nextBoolean() ? 1 : 2; + } else if (t1) { + return 1; + } else if (t2) { + return 2; + } + return -1; + } + + /** Gibt zurück, ob Start, Finish und mindestens 1 Checkpoint für den Track gesetzt sind. */ + public boolean isTrackReady(int track) { + return getStartLocation(track) != null + && getFinishLocation(track) != null + && !getOrderedCheckpoints(track).isEmpty(); + } + + private void startParticleTask() { + new BukkitRunnable() { + @Override + public void run() { + for (UUID uuid : startTime.keySet()) { + Player p = Bukkit.getPlayer(uuid); + if (p == null) continue; + + int track = activeTrack.getOrDefault(uuid, 1); + int nextIdx = nextCheckpointIndex.getOrDefault(uuid, 0); + List cps = getOrderedCheckpoints(track); + + 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(track); + 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) return; + + int track = activeTrack.getOrDefault(player.getUniqueId(), 1); + List cps = getOrderedCheckpoints(track); + if (reachedIndex >= cps.size()) return; + + 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; + + int track = activeTrack.getOrDefault(player.getUniqueId(), 1); + if (nextCheckpointIndex.getOrDefault(player.getUniqueId(), 0) < getOrderedCheckpoints(track).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! §8(Strecke " + track + ")"); + 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()); + activeTrack.remove(player.getUniqueId()); + } + + // ───────────────────────────────────────────────────────────────────────── + // Bestzeiten + // ───────────────────────────────────────────────────────────────────────── + + private void saveBestTime(Player player, double time) { + int track = activeTrack.getOrDefault(player.getUniqueId(), 1); + String path = "besttimes." + track + "." + 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."); + } + } + + /** + * Löscht NUR die Bestzeiten-Liste (besttimes + names). + * Die Strecken-Konfiguration (tracks) bleibt vollständig erhalten. + * Wird durch "/nexus parkour clear" aufgerufen. + */ + public void clearStats() { + config.set("besttimes", null); + config.set("names", null); + save(); + } + + public void clearStats(int track) { + config.set("besttimes." + track, null); + save(); + } + + public String getTopTen() { + // Fallback: kombiniert beide Tracks (Legacy-Support für %nexuslobby_parkour_top%) + return getTopTen(0); + } + + public String getTopTen(int track) { + String trackLabel = track == 1 ? " §8(Strecke 1)" : track == 2 ? " §8(Strecke 2)" : ""; + + Map allTimes = new HashMap<>(); + + if (track == 0) { + // Alle Tracks kombiniert (nimmt die beste Zeit je Spieler über alle Tracks) + for (int t = 1; t <= 2; t++) { + String section = "besttimes." + t; + if (config.contains(section) && config.getConfigurationSection(section) != null) { + for (String uuidStr : config.getConfigurationSection(section).getKeys(false)) { + double time = config.getDouble(section + "." + uuidStr); + allTimes.merge(uuidStr, time, Math::min); + } + } + } + } else { + String section = "besttimes." + track; + if (!config.contains(section) || config.getConfigurationSection(section) == null) { + return "§6§l🏆 TOP 10 PARKOUR" + trackLabel + "\n§7Noch keine Rekorde."; + } + for (String uuidStr : config.getConfigurationSection(section).getKeys(false)) { + allTimes.put(uuidStr, config.getDouble(section + "." + uuidStr)); + } + } + + if (allTimes.isEmpty()) + return "§6§l🏆 TOP 10 PARKOUR" + trackLabel + "\n§7Noch keine Rekorde."; + + List> sortedList = allTimes.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .limit(10) + .toList(); + + StringBuilder builder = new StringBuilder("§6§l🏆 TOP 10 PARKOUR" + trackLabel); + 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(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Hilfsmethoden + // ───────────────────────────────────────────────────────────────────────── + + public boolean isIngame(Player player) { return startTime.containsKey(player.getUniqueId()); } + public Location getCheckpoint(Player player) { return lastCheckpointLoc.get(player.getUniqueId()); } + + /** Gibt die Strecken-Info als lesbaren String aus (für Admin-Feedback). */ + public String getTrackInfo() { + StringBuilder sb = new StringBuilder("§8[§6Parkour§8] §7Track-Status:\n"); + for (int t = 1; t <= 2; t++) { + int cps = getOrderedCheckpoints(t).size(); + boolean ready = isTrackReady(t); + sb.append(" §eStrecke ").append(t).append(": ") + .append(ready ? "§a✔" : "§c✘") + .append(" §7(").append(cps).append(" Checkpoints)"); + if (t < 2) sb.append("\n"); + } + return sb.toString(); + } } \ No newline at end of file