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
+
+
+
+
+
+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.
+
+
+
+
+
+
+---
+
+## 🎯 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