diff --git a/src/main/java/de/nexuslobby/NexusLobby.java b/src/main/java/de/nexuslobby/NexusLobby.java index 9bfc815..9153e38 100644 --- a/src/main/java/de/nexuslobby/NexusLobby.java +++ b/src/main/java/de/nexuslobby/NexusLobby.java @@ -69,6 +69,7 @@ public class NexusLobby extends JavaPlugin implements Listener { private BorderModule borderModule; private ParkourManager parkourManager; private SoccerModule soccerModule; + private ArmorStandLookAtModule armorStandLookAtModule; private ConversationManager conversationManager; @@ -135,7 +136,7 @@ public class NexusLobby extends JavaPlugin implements Listener { ServerChecker.startGlobalChecker(); - new ArmorStandLookAtModule(); + armorStandLookAtModule = new ArmorStandLookAtModule(); startAutoConversationTimer(); @@ -192,7 +193,10 @@ public class NexusLobby extends JavaPlugin implements Listener { } ServerChecker.startGlobalChecker(); - new ArmorStandLookAtModule(); + // FIX: ArmorStandLookAtModule als Feld tracken. Bukkit.getScheduler().cancelTasks() + // am Anfang von reloadPlugin() cancelt den alten Task bereits – hier nur + // neu starten und Referenz aktualisieren, damit kein doppelter Task läuft. + armorStandLookAtModule = new ArmorStandLookAtModule(); startAutoConversationTimer(); getLogger().info("Plugin Reload abgeschlossen. Änderungen wurden übernommen."); diff --git a/src/main/java/de/nexuslobby/commands/LobbyTabCompleter.java b/src/main/java/de/nexuslobby/commands/LobbyTabCompleter.java index 8ef7d0e..675d1fd 100644 --- a/src/main/java/de/nexuslobby/commands/LobbyTabCompleter.java +++ b/src/main/java/de/nexuslobby/commands/LobbyTabCompleter.java @@ -28,65 +28,111 @@ public class LobbyTabCompleter implements TabCompleter { List suggestions = new ArrayList<>(); String cmdName = command.getName().toLowerCase(); - // --- NexusLobby Hauptbefehl (/nexus) --- + // ── NexusLobby Hauptbefehl (/nexus oder /nexuslobby) ───────────────── if (cmdName.equals("nexuslobby") || cmdName.equals("nexus")) { + + // /nexuslobby if (args.length == 1) { if (sender.hasPermission("nexuslobby.admin")) { - suggestions.addAll(Arrays.asList("reload", "setspawn", "silentjoin", "parkour", "ball")); // NEU: ball + suggestions.addAll(Arrays.asList( + "reload", "setspawn", "silentjoin", "parkour", "ball" + )); } suggestions.add("sb"); + + // /nexuslobby } else if (args.length == 2) { - if (args[0].equalsIgnoreCase("sb")) { - suggestions.addAll(Arrays.asList("on", "off")); - if (sender.hasPermission("nexuslobby.scoreboard.admin")) { - suggestions.addAll(Arrays.asList("admin", "spieler")); + switch (args[0].toLowerCase()) { + case "sb" -> { + suggestions.addAll(Arrays.asList("on", "off")); + if (sender.hasPermission("nexuslobby.scoreboard.admin")) + suggestions.addAll(Arrays.asList("admin", "spieler")); } - } else if (args[0].equalsIgnoreCase("silentjoin")) { - suggestions.addAll(Arrays.asList("on", "off")); - } else if (args[0].equalsIgnoreCase("parkour")) { - suggestions.addAll(Arrays.asList("setstart", "setfinish", "setcheckpoint", "reset", "clear", "removeall")); - } else if (args[0].equalsIgnoreCase("ball")) { - if (sender.hasPermission("nexuslobby.admin")) { - suggestions.addAll(Arrays.asList("setspawn", "respawn", "remove")); + case "silentjoin" -> suggestions.addAll(Arrays.asList("on", "off")); + case "parkour" -> suggestions.addAll(Arrays.asList( + "setstart", "setfinish", "setcheckpoint", "reset", "clear", "removeall" + )); + case "ball" -> { + if (sender.hasPermission("nexuslobby.admin")) { + suggestions.addAll(Arrays.asList( + "setspawn", "respawn", "remove", "goal", "score" + )); + } } } + + // /nexuslobby } else if (args.length == 3) { - if (args[0].equalsIgnoreCase("parkour") && args[1].equalsIgnoreCase("setcheckpoint")) { - suggestions.addAll(Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8", "9")); + switch (args[0].toLowerCase()) { + case "parkour" -> { + if (args[1].equalsIgnoreCase("setcheckpoint")) + suggestions.addAll(Arrays.asList("1","2","3","4","5","6","7","8","9")); + } + case "ball" -> { + switch (args[1].toLowerCase()) { + // /nexuslobby ball goal + case "goal" -> suggestions.addAll(Arrays.asList( + "pos1", "pos2", "save", "delete", "list", "show", "debug" + )); + // /nexuslobby ball score + case "score" -> suggestions.add("reset"); + } + } + } + + // /nexuslobby + } else if (args.length == 4) { + if (args[0].equalsIgnoreCase("ball") && args[1].equalsIgnoreCase("goal")) { + switch (args[2].toLowerCase()) { + // /nexuslobby ball goal save → Tor-Name (Freitext) + case "save" -> suggestions.add(""); + // /nexuslobby ball goal delete + case "delete" -> { + // Tor-Namen aus Config laden und vorschlagen + var section = NexusLobby.getInstance().getConfig() + .getConfigurationSection("ball.goals"); + if (section != null) suggestions.addAll(section.getKeys(false)); + } + } + } + + // /nexuslobby ball goal save → Team 1 oder 2 + } else if (args.length == 5) { + if (args[0].equalsIgnoreCase("ball") + && args[1].equalsIgnoreCase("goal") + && args[2].equalsIgnoreCase("save")) { + suggestions.addAll(Arrays.asList("1", "2")); } } - } - - // --- Hologram Befehl --- + } + + // ── Hologram Befehl ─────────────────────────────────────────────────── else if (cmdName.equals("holo")) { if (args.length == 1) { suggestions.addAll(Arrays.asList("create", "delete")); } else if (args.length == 2 && args[0].equalsIgnoreCase("delete")) { - if (hologramModule != null) { + if (hologramModule != null) suggestions.addAll(hologramModule.getHologramIds()); - } } - } - - // --- Wartungsmodus --- + } + + // ── Wartungsmodus ───────────────────────────────────────────────────── else if (cmdName.equals("maintenance")) { - if (args.length == 1) { + if (args.length == 1) suggestions.addAll(Arrays.asList("on", "off")); - } - } - - // --- Portalsystem --- + } + + // ── Portalsystem ────────────────────────────────────────────────────── else if (cmdName.equals("portal")) { if (args.length == 1) { suggestions.addAll(Arrays.asList("create", "delete", "list")); } else if (args.length == 2 && args[0].equalsIgnoreCase("delete")) { - if (portalManager != null) { + if (portalManager != null) suggestions.addAll(portalManager.getPortalNames()); - } } - } - - // --- MapArt --- + } + + // ── MapArt ──────────────────────────────────────────────────────────── else if (cmdName.equals("mapart")) { if (args.length == 1) { suggestions.add("https://"); @@ -95,14 +141,13 @@ public class LobbyTabCompleter implements TabCompleter { } } - // --- Intro System --- + // ── Intro System ────────────────────────────────────────────────────── else if (cmdName.equals("intro")) { - if (args.length == 1) { + if (args.length == 1) suggestions.addAll(Arrays.asList("add", "clear", "start")); - } } - // --- WorldBorder --- + // ── WorldBorder ─────────────────────────────────────────────────────── else if (cmdName.equals("border")) { if (args.length == 1) { suggestions.addAll(Arrays.asList("circle", "square", "disable")); @@ -110,59 +155,49 @@ public class LobbyTabCompleter implements TabCompleter { suggestions.addAll(Arrays.asList("10", "25", "50", "100", "250")); } } - - // --- NexusCmd / ArmorStandTools --- - else if (cmdName.equals("nexuscmd") || cmdName.equals("ncmd") || cmdName.equals("ascmd") || cmdName.equals("conv")) { + + // ── NexusCmd / ArmorStandTools ──────────────────────────────────────── + else if (cmdName.equals("nexuscmd") || cmdName.equals("ncmd") + || cmdName.equals("ascmd") || cmdName.equals("conv")) { if (args.length == 1) { suggestions.addAll(Arrays.asList("add", "remove", "list", "name", "lookat", "conv", "say")); - } - else if (args.length == 2) { - if (args[0].equalsIgnoreCase("add")) { - suggestions.addAll(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")); - } else if (args[0].equalsIgnoreCase("name")) { - suggestions.addAll(Arrays.asList("", "none")); - } else if (args[0].equalsIgnoreCase("conv")) { - suggestions.addAll(Arrays.asList("select1", "select2", "select3", "select4", "link", "unlink", "start")); - } else if (args[0].equalsIgnoreCase("remove")) { - suggestions.addAll(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "all")); - } else if (args[0].equalsIgnoreCase("say")) { - suggestions.add(""); + } else if (args.length == 2) { + switch (args[0].toLowerCase()) { + case "add" -> suggestions.addAll(Arrays.asList("0","1","2","3","4","5","6","7","8","9")); + case "name" -> suggestions.addAll(Arrays.asList("", "none")); + case "conv" -> suggestions.addAll(Arrays.asList("select1","select2","select3","select4","link","unlink","start")); + case "remove" -> suggestions.addAll(Arrays.asList("0","1","2","3","4","5","6","7","8","9","all")); + case "say" -> suggestions.add(""); } - } - else if (args.length == 3) { + } else if (args.length == 3) { if (args[0].equalsIgnoreCase("add")) { - suggestions.addAll(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")); - } else if (args[0].equalsIgnoreCase("conv")) { - if (args[1].equalsIgnoreCase("start") || args[1].equalsIgnoreCase("link")) { - if (NexusLobby.getInstance().getConversationManager() != null) { - suggestions.addAll(NexusLobby.getInstance().getConversationManager().getConversationIds()); - } - } + suggestions.addAll(Arrays.asList("0","1","2","3","4","5","6","7","8","9")); + } else if (args[0].equalsIgnoreCase("conv") + && (args[1].equalsIgnoreCase("start") || args[1].equalsIgnoreCase("link"))) { + if (NexusLobby.getInstance().getConversationManager() != null) + suggestions.addAll(NexusLobby.getInstance().getConversationManager().getConversationIds()); } - } - else if (args.length == 4 && args[0].equalsIgnoreCase("add")) { + } else if (args.length == 4 && args[0].equalsIgnoreCase("add")) { suggestions.addAll(Arrays.asList("bungee", "console", "player")); - } - else if (args.length == 5 && args[0].equalsIgnoreCase("add") && args[3].equalsIgnoreCase("bungee")) { - if (NexusLobby.getInstance().getConfig().getConfigurationSection("servers") != null) { - suggestions.addAll(NexusLobby.getInstance().getConfig().getConfigurationSection("servers").getKeys(false)); - } + } else if (args.length == 5 && args[0].equalsIgnoreCase("add") + && args[3].equalsIgnoreCase("bungee")) { + var section = NexusLobby.getInstance().getConfig().getConfigurationSection("servers"); + if (section != null) suggestions.addAll(section.getKeys(false)); } } - // --- ArmorStandTools Alternate --- + // ── ArmorStandTools Alternate ───────────────────────────────────────── else if (cmdName.equals("astools") || cmdName.equals("nt") || cmdName.equals("ntools")) { if (args.length == 1) { - suggestions.addAll(Arrays.asList("dynamic", "lookat", "addplayer", "addconsole", "remove", "reload", "say")); - } - else if (args.length == 2 && args[0].equalsIgnoreCase("say")) { - suggestions.add(""); - } - else if (args.length == 2 && (args[0].equalsIgnoreCase("addplayer") || args[0].equalsIgnoreCase("addconsole"))) { - suggestions.addAll(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")); - } - else if (args.length == 3 && (args[0].equalsIgnoreCase("addplayer") || args[0].equalsIgnoreCase("addconsole"))) { - suggestions.addAll(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")); + suggestions.addAll(Arrays.asList("dynamic","lookat","addplayer","addconsole","remove","reload","say")); + } else if (args.length == 2) { + if (args[0].equalsIgnoreCase("say")) + suggestions.add(""); + else if (args[0].equalsIgnoreCase("addplayer") || args[0].equalsIgnoreCase("addconsole")) + suggestions.addAll(Arrays.asList("0","1","2","3","4","5","6","7","8","9")); + } else if (args.length == 3) { + if (args[0].equalsIgnoreCase("addplayer") || args[0].equalsIgnoreCase("addconsole")) + suggestions.addAll(Arrays.asList("0","1","2","3","4","5","6","7","8","9")); } } diff --git a/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java b/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java index 249d05d..71d624f 100644 --- a/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java +++ b/src/main/java/de/nexuslobby/commands/NexusLobbyCommand.java @@ -82,6 +82,12 @@ public class NexusLobbyCommand implements CommandExecutor { player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1f, 1.5f); break; + case "cleanbubbles": + // Bereinigt alle hängengebliebenen Sprechblasen-ArmorStands im gesamten Server. + if (!player.hasPermission("nexuslobby.admin")) return noPerm(player); + handleCleanBubbles(player); + break; + case "setspawn": if (!player.hasPermission("nexuslobby.admin")) return noPerm(player); Location loc = player.getLocation(); @@ -187,6 +193,18 @@ public class NexusLobbyCommand implements CommandExecutor { player.sendMessage(de.nexuslobby.utils.LangManager.get("parkour_start_set")); } + private void handleCleanBubbles(Player player) { + de.nexuslobby.modules.armorstandtools.ConversationManager cm = + NexusLobby.getInstance().getConversationManager(); + if (cm == null) { + player.sendMessage("§8[§6Nexus§8] §cConversationManager ist nicht aktiv."); + return; + } + cm.clearHangingBubbles(); + player.sendMessage("§8[§6Nexus§8] §aAlle hängengebliebenen Sprechblasen wurden entfernt."); + player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 2f); + } + private boolean noPerm(Player player) { player.sendMessage(de.nexuslobby.utils.LangManager.get("no_permission")); return true; @@ -236,6 +254,7 @@ public class NexusLobbyCommand implements CommandExecutor { player.sendMessage(de.nexuslobby.utils.LangManager.get("info_setspawn")); player.sendMessage(de.nexuslobby.utils.LangManager.get("info_scoreboard")); player.sendMessage(de.nexuslobby.utils.LangManager.get("info_reload")); + player.sendMessage("§e/nexuslobby cleanbubbles §7- Hängende Sprechblasen entfernen"); player.sendMessage(de.nexuslobby.utils.LangManager.get("info_footer")); } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/ProtectionModule.java b/src/main/java/de/nexuslobby/modules/ProtectionModule.java index 42d4ef9..27fd22a 100644 --- a/src/main/java/de/nexuslobby/modules/ProtectionModule.java +++ b/src/main/java/de/nexuslobby/modules/ProtectionModule.java @@ -6,11 +6,14 @@ import de.nexuslobby.commands.BuildCommand; import org.bukkit.Bukkit; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.EventHandler; import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockExplodeEvent; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityExplodeEvent; import org.bukkit.event.entity.EntityPickupItemEvent; import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerInteractEvent; @@ -30,7 +33,11 @@ public class ProtectionModule implements Module, Listener { } @Override - public void onDisable() {} + public void onDisable() { + // FIX: Listener nach dem Deaktivieren abmelden, damit nach /reload + // keine doppelten Events ausgelöst werden. + HandlerList.unregisterAll(this); + } @EventHandler public void onBlockBreak(BlockBreakEvent event) { @@ -91,4 +98,22 @@ public class ProtectionModule implements Module, Listener { } } } + + // FIX: Fehlender Explosionsschutz - settings.yml hat allowExplosions: false, + // aber bisher gab es keinen Handler dafür. + @EventHandler + public void onEntityExplode(EntityExplodeEvent event) { + if (!getSettings().getBoolean("allowExplosions", false)) { + event.blockList().clear(); + event.setCancelled(true); + } + } + + @EventHandler + public void onBlockExplode(BlockExplodeEvent event) { + if (!getSettings().getBoolean("allowExplosions", false)) { + event.blockList().clear(); + event.setCancelled(true); + } + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/ScoreboardModule.java b/src/main/java/de/nexuslobby/modules/ScoreboardModule.java index 511dbd0..41c5338 100644 --- a/src/main/java/de/nexuslobby/modules/ScoreboardModule.java +++ b/src/main/java/de/nexuslobby/modules/ScoreboardModule.java @@ -162,6 +162,7 @@ public class ScoreboardModule implements Module, Listener { @Override public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); for (Player player : Bukkit.getOnlinePlayers()) { player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); } diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ASTListener.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ASTListener.java index 25c5c24..a8bf018 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ASTListener.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ASTListener.java @@ -70,32 +70,34 @@ public class ASTListener implements Listener { /** * Prüft, ob der ArmorStand Dialog-Tags hat und startet das Gespräch über den Manager. - * Nutzt nun die Gruppen-Logik (z.B. owner_suche). + * FIX: Liest jetzt alle Partner-Tags (conv_partner, conv_partner2, conv_partner3) + * und verwendet playConversationGroup statt nur die 2-NPC-Methode. + * Vorher wurden Gruppen-Gespräche mit 3-4 NPCs als 2-NPC-Gespräch ausgeführt. */ private void checkAndTriggerDialog(ArmorStand as, Player p) { String groupName = null; - String partnerUUIDString = null; + String partner1 = null, partner2 = null, partner3 = null; for (String tag : as.getScoreboardTags()) { - // conv_id enthält jetzt den Gruppennamen aus der conversations.yml - if (tag.startsWith("conv_id:")) groupName = tag.split(":")[1]; - if (tag.startsWith("conv_partner:")) partnerUUIDString = tag.split(":")[1]; + if (tag.startsWith("conv_id:")) groupName = tag.split(":", 2)[1]; + if (tag.startsWith("conv_partner:")) partner1 = tag.split(":", 2)[1]; + if (tag.startsWith("conv_partner2:")) partner2 = tag.split(":", 2)[1]; + if (tag.startsWith("conv_partner3:")) partner3 = tag.split(":", 2)[1]; } - if (groupName != null && partnerUUIDString != null) { - try { - UUID partnerUUID = UUID.fromString(partnerUUIDString); - - // Wir rufen playConversation auf. Der Manager entscheidet selbst - // anhand der Uhrzeit, ob er morgens, mittags oder nachts abspielt. - NexusLobby.getInstance().getConversationManager().playConversation( - as.getUniqueId(), - partnerUUID, - groupName - ); - } catch (Exception ignored) { - // Falls die UUID oder Gruppe ungültig ist - } + if (groupName == null || partner1 == null) return; + + try { + java.util.List participants = new java.util.ArrayList<>(); + participants.add(as.getUniqueId()); + participants.add(UUID.fromString(partner1)); + if (partner2 != null) participants.add(UUID.fromString(partner2)); + if (partner3 != null) participants.add(UUID.fromString(partner3)); + + NexusLobby.getInstance().getConversationManager() + .playConversationGroup(participants, groupName); + } catch (Exception ignored) { + // Ungültige UUID oder fehlende Gruppe – still ignorieren } } diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandTool.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandTool.java index 95588bf..41c5eac 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandTool.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ArmorStandTool.java @@ -99,8 +99,11 @@ public enum ArmorStandTool { } public static ArmorStandTool get(ItemStack item) { - if (item == null || !item.hasItemMeta() || !item.getItemMeta().hasDisplayName()) return null; - String name = ChatColor.stripColor(item.getItemMeta().getDisplayName()).replace(" ", "_"); + if (item == null || !item.hasItemMeta()) return null; + ItemMeta meta = item.getItemMeta(); + // FIX: getItemMeta() kann null zurückgeben; hasDisplayName() vor getDisplayName() prüfen + if (meta == null || !meta.hasDisplayName()) return null; + String name = ChatColor.stripColor(meta.getDisplayName()).replace(" ", "_"); if (name.equals("Pose_Editor_öffnen")) return OPEN_POSE_EDITOR; if (name.equals("Gesprächs-Setup")) return CONV_SETUP; try { return valueOf(name); } catch (Exception e) { return null; } diff --git a/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java b/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java index eec2c52..870305a 100644 --- a/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java +++ b/src/main/java/de/nexuslobby/modules/armorstandtools/ConversationManager.java @@ -45,13 +45,15 @@ public class ConversationManager { /** * 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() { int count = 0; for (World world : Bukkit.getWorlds()) { for (ArmorStand as : world.getEntitiesByClass(ArmorStand.class)) { - if (as.getScoreboardTags().contains("nexus_bubble") || - (!as.isVisible() && as.isMarker() && as.getCustomName() != null)) { + if (as.getScoreboardTags().contains("nexus_bubble")) { as.remove(); count++; } diff --git a/src/main/java/de/nexuslobby/modules/ball/SoccerModule.java b/src/main/java/de/nexuslobby/modules/ball/SoccerModule.java index 1fe51a1..6b11c2f 100644 --- a/src/main/java/de/nexuslobby/modules/ball/SoccerModule.java +++ b/src/main/java/de/nexuslobby/modules/ball/SoccerModule.java @@ -7,6 +7,7 @@ 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; @@ -23,375 +24,719 @@ import org.bukkit.util.Vector; import java.net.MalformedURLException; import java.net.URL; -import java.util.UUID; -import java.util.Objects; +import java.util.*; public class SoccerModule implements Module, Listener, CommandExecutor { - // Ball Konstanten - private static final String TEXTURE_URL = "http://textures.minecraft.net/texture/451f8cfcfb85d77945dc6a3618414093e70436b46d2577b28c727f1329b7265e"; - private static final String BALL_TAG = "nexusball_entity"; + // ========================================================================= + // 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"; - - // Physik Konstanten - 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; - - // Particle Konstanten - 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; - - private ArmorStand ball; - private Location spawnLocation; - private long lastMoveTime; - @Override - public String getName() { return "Soccer"; } + 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; + + 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; + + // ── 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()); - if (NexusLobby.getInstance().getCommand("nexuslobby") != null) { - Objects.requireNonNull(NexusLobby.getInstance().getCommand("nexuslobby")).setExecutor(this); - } - loadConfigLocation(); + loadGoals(); + scores.put(1, 0); + scores.put(2, 0); - // Optimiertes Cleanup-System: 3 Phasen statt 6 removeAllOldBalls(); - Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), () -> { removeAllOldBalls(); spawnBall(); - }, 40L); // Nach 2 Sekunden - - Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), this::removeAllOldBalls, 100L); // Finaler Check nach 5 Sekunden + }, 40L); + Bukkit.getScheduler().runTaskLater(NexusLobby.getInstance(), this::removeAllOldBalls, 100L); - // Haupt-Physik & Anti-Duplikat System + // ── Physik-Tick ──────────────────────────────────────────────────────── Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), () -> { - // Anti-Duplikat-Check (optimiert: nur in Ball-Welt) - if (ball != null && ball.isValid()) { - removeDuplicateBalls(); - } - + if (ball != null && ball.isValid()) removeDuplicateBalls(); if (ball == null || !ball.isValid()) return; - Vector vel = ball.getVelocity(); - double speed = vel.length(); + // ── 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(); - // Automatischer Respawn bei Inaktivität oder Void - long respawnDelayMs = NexusLobby.getInstance().getConfig().getLong("ball.respawn_delay", 60) * 1000; - if (System.currentTimeMillis() - lastMoveTime > respawnDelayMs || ball.getLocation().getY() < VOID_THRESHOLD) { + // ── SCHRITT 3: Nach Physik die neue Position lesen ───────────────── + // Minecraft hat jetzt die Position um vel verschoben. + // Da getLocation() einen Tick verzögert ist, lesen wir hier noch + // currentPos – aber über den Velocity-Vektor wissen wir die neue Pos. + // Wir nutzen deshalb: newPos = currentPos + velocity (vor Drag) + // Das ist die tatsächliche neue Ballposition nach diesem Tick. + Location newPos = currentPos.clone().add(vel); + + // ── 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; + } } /** - * Optimierte Dribbel-Logik: Ball folgt nahen Spielern + * 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; - for (Entity nearby : ball.getNearbyEntities(DRIBBLE_DETECTION_RADIUS, DRIBBLE_HEIGHT, DRIBBLE_DETECTION_RADIUS)) { - if (nearby instanceof Player p) { - Vector direction = ball.getLocation().toVector().subtract(p.getLocation().toVector()); - if (direction.lengthSquared() > 0) { - direction.normalize(); - direction.setY(DRIBBLE_LIFT); - ball.setVelocity(direction.multiply(DRIBBLE_FORCE)); + 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(); } } } } - /** - * Entfernt Duplikate in der Umgebung des aktiven Balls (Performance-optimiert) - */ - private void removeDuplicateBalls() { - if (ball == null || !ball.isValid() || ball.getLocation().getWorld() == null) return; - - for (Entity entity : ball.getLocation().getWorld().getNearbyEntities(ball.getLocation(), 50, 50, 50)) { - if (entity instanceof ArmorStand stand && isBallEntity(stand)) { - if (!stand.getUniqueId().equals(ball.getUniqueId())) { - stand.remove(); - } - } - } - } - - /** - * Entfernt alle Ball-Entities (nur in relevanter Welt wenn Spawn gesetzt) - */ - private void removeAllOldBalls() { - int removed = 0; - - // Wenn Spawn-Location gesetzt ist, nur diese Welt durchsuchen (Performance!) - if (spawnLocation != null && spawnLocation.getWorld() != null) { - removed = cleanupBallsInWorld(spawnLocation.getWorld()); - } else { - // Sonst alle Welten durchsuchen - for (World world : Bukkit.getWorlds()) { - removed += cleanupBallsInWorld(world); - } - } - - if (removed > 0) { - Bukkit.getLogger().info("[NexusLobby] " + removed + " alte Ball-Entities entfernt."); - } - } - - /** - * Cleanup für eine einzelne Welt - */ - private int cleanupBallsInWorld(World world) { - int removed = 0; - for (Entity entity : world.getEntities()) { - if (entity instanceof ArmorStand stand && isBallEntity(stand)) { - if (ball == null || !stand.getUniqueId().equals(ball.getUniqueId())) { - stand.remove(); - removed++; - } - } - } - return removed; - } - - /** - * Prüft ob ein ArmorStand ein Ball ist (Mehrere Erkennungsmethoden) - */ - private boolean isBallEntity(ArmorStand stand) { - // Methode 1: Tag-basiert (primär) - if (stand.getScoreboardTags().contains(BALL_TAG)) { - return true; - } - - // Methode 2: Name-basiert - if (stand.getCustomName() != null && stand.getCustomName().equals(BALL_NAME)) { - return true; - } - - // Methode 3: Profil-basiert (Kopf-Textur) - if (stand.getEquipment() != null && stand.getEquipment().getHelmet() != null) { - ItemStack helmet = stand.getEquipment().getHelmet(); - if (helmet.getType() == Material.PLAYER_HEAD && helmet.hasItemMeta()) { - SkullMeta meta = (SkullMeta) helmet.getItemMeta(); - if (meta.hasOwner() && meta.getOwnerProfile() != null) { - if (BALL_NAME.equals(meta.getOwnerProfile().getName())) { - return true; - } - } - } - } - - // Methode 4: Eigenschaften-basiert (nur wenn Spawn gesetzt) - if (spawnLocation != null && spawnLocation.getWorld() != null && - stand.getWorld().equals(spawnLocation.getWorld()) && - stand.isSmall() && stand.isInvisible() && !stand.hasBasePlate()) { - - double distance = stand.getLocation().distance(spawnLocation); - if (distance < CLEANUP_RADIUS && stand.getEquipment() != null && - stand.getEquipment().getHelmet() != null && - stand.getEquipment().getHelmet().getType() == Material.PLAYER_HEAD) { - return true; - } - } - - return false; - } - private void handleWallBounce(Vector vel) { if (vel.lengthSquared() < 0.001) return; - Location loc = ball.getLocation(); - Block nextX = loc.clone().add(vel.getX() * WALL_CHECK_DISTANCE, 0.5, 0).getBlock(); - Block nextZ = loc.clone().add(0, 0.5, vel.getZ() * WALL_CHECK_DISTANCE).getBlock(); - + 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 (nextX.getType().isSolid()) { - vel.setX(-vel.getX() * WALL_BOUNCE_DAMPING); - bounced = true; - } - if (nextZ.getType().isSolid()) { - vel.setZ(-vel.getZ() * WALL_BOUNCE_DAMPING); - bounced = true; - } - + 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(ball.getLocation(), Sound.BLOCK_WOOD_BREAK, 0.6f, 1.3f); + 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 world = loc.getWorld(); - if (world == null) return; - - if (speed > PARTICLE_SPEED_HIGH) { - world.spawnParticle(Particle.SONIC_BOOM, loc, 1, 0, 0, 0, 0); - } else if (speed > PARTICLE_SPEED_MEDIUM) { - world.spawnParticle(Particle.CRIT, loc, 3, 0.1, 0.1, 0.1, 0.08); - } else { - world.spawnParticle(Particle.SMOKE, loc, 1, 0.05, 0, 0.05, 0.02); - } + 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) { - // Keine Warnung mehr in der Konsole ausgeben - return; - } - + if (spawnLocation == null || spawnLocation.getWorld() == null) return; if (ball != null && ball.isValid()) return; - - Location spawnLoc = spawnLocation.clone(); - ball = (ArmorStand) spawnLoc.getWorld().spawnEntity(spawnLoc, 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(false); - ball.addScoreboardTag(BALL_TAG); - - ItemStack ballHead = getSoccerHead(); - if (ball.getEquipment() != null) { - ball.getEquipment().setHelmet(ballHead); - } - + 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(false); 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!"); - } + 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; } - public void respawnBall() { - if (ball != null && ball.isValid()) { - ball.remove(); - ball = null; + // ========================================================================= + // 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(); } - removeAllOldBalls(); - spawnBall(); } + 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) { - Vector shootDir = p.getLocation().getDirection(); - if (shootDir.lengthSquared() > 0) { - shootDir.normalize().multiply(KICK_FORCE).setY(KICK_LIFT); - ball.setVelocity(shootDir); - ball.getWorld().playSound(ball.getLocation(), Sound.ENTITY_ZOMBIE_ATTACK_IRON_DOOR, 0.6f, 1.5f); - lastMoveTime = System.currentTimeMillis(); - } - } + 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(); + 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); - } + if (ball != null && event.getRightClicked().equals(ball)) event.setCancelled(true); } - private void loadConfigLocation() { - FileConfiguration config = NexusLobby.getInstance().getConfig(); - if (config.contains("ball.spawn")) { - spawnLocation = config.getLocation("ball.spawn"); - } - } + // ========================================================================= + // COMMANDS + // ========================================================================= @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player p)) { - sender.sendMessage("§cNur Spieler können diesen Befehl ausführen."); - return true; - } - - if (!p.hasPermission("nexuslobby.admin")) { - p.sendMessage("§cKeine Berechtigung!"); - return true; - } + 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; - if (args.length >= 2 && args[0].equalsIgnoreCase("ball")) { - switch (args[1].toLowerCase()) { - case "setspawn" -> { - spawnLocation = p.getLocation(); - NexusLobby.getInstance().getConfig().set("ball.spawn", spawnLocation); - NexusLobby.getInstance().saveConfig(); - respawnBall(); - p.sendMessage("§8[§6Nexus§8] §aBall-Spawn gesetzt und Ball respawnt!"); - return true; - } - case "respawn" -> { - respawnBall(); - p.sendMessage("§8[§6Nexus§8] §eBall manuell respawnt."); - return true; - } - case "remove" -> { - if (ball != null) { - ball.remove(); - ball = null; - } - removeAllOldBalls(); - p.sendMessage("§8[§6Nexus§8] §cBall entfernt und Cleanup durchgeführt."); - return true; - } - default -> { - p.sendMessage("§8[§6Nexus§8] §7Verwendung:"); - p.sendMessage("§e/nexuslobby ball setspawn §7- Setzt den Ball-Spawn"); - p.sendMessage("§e/nexuslobby ball respawn §7- Spawnt den Ball neu"); - p.sendMessage("§e/nexuslobby ball remove §7- Entfernt den Ball"); - return true; + 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); } - return false; } - @Override - public void onDisable() { - if (ball != null && ball.isValid()) { - ball.remove(); - ball = null; + 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 5ede9e0..5685f46 100644 --- a/src/main/java/de/nexuslobby/modules/border/BorderCommand.java +++ b/src/main/java/de/nexuslobby/modules/border/BorderCommand.java @@ -40,10 +40,21 @@ public class BorderCommand implements CommandExecutor { } try { double radius = Double.parseDouble(args[1]); + Location loc = p.getLocation(); + config.set("worldborder.type", "CIRCLE"); - config.set("worldborder.center", p.getLocation()); - config.set("worldborder.radius", radius); 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."); @@ -57,10 +68,21 @@ public class BorderCommand implements CommandExecutor { p.sendMessage("§8[§6Nexus§8] §cBitte markiere erst 2 Punkte mit der Portalwand!"); return true; } + config.set("worldborder.type", "SQUARE"); - config.set("worldborder.pos1", l1); - config.set("worldborder.pos2", l2); 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" -> { @@ -74,13 +96,13 @@ public class BorderCommand implements CommandExecutor { } 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 1751a61..8f5e7fd 100644 --- a/src/main/java/de/nexuslobby/modules/border/BorderModule.java +++ b/src/main/java/de/nexuslobby/modules/border/BorderModule.java @@ -9,19 +9,29 @@ 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; - private Location pos1, pos2, center; + // 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"; } @@ -34,74 +44,96 @@ public class BorderModule implements Module, Listener { @Override public void onDisable() { - // Aufräumarbeiten falls nötig + // 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 NexusLobby.reloadPlugin() aufgerufen. + * 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(); - - // Pfad in der config.yml: worldborder.enabled etc. + this.enabled = config.getBoolean("worldborder.enabled", false); - this.type = config.getString("worldborder.type", "CIRCLE"); - this.radius = config.getDouble("worldborder.radius", 50.0); - - // Locations laden - this.center = config.getLocation("worldborder.center"); - this.pos1 = config.getLocation("worldborder.pos1"); - this.pos2 = config.getLocation("worldborder.pos2"); + 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 die Block-Koordinaten ändern + + // 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(); - - // Admins und Spectators ignorieren - if (player.hasPermission("nexuslobby.admin") || player.getGameMode().name().equals("SPECTATOR")) return; + + // 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.equalsIgnoreCase("CIRCLE") && center != null) { - if (to.getWorld().equals(center.getWorld())) { - double distSq = Math.pow(to.getX() - center.getX(), 2) + Math.pow(to.getZ() - center.getZ(), 2); - if (distSq > Math.pow(radius, 2)) outside = true; + 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.equalsIgnoreCase("SQUARE") && pos1 != null && pos2 != null) { - if (to.getWorld().equals(pos1.getWorld())) { - double minX = Math.min(pos1.getX(), pos2.getX()); - double maxX = Math.max(pos1.getX(), pos2.getX()); - double minZ = Math.min(pos1.getZ(), pos2.getZ()); - double maxZ = Math.max(pos1.getZ(), pos2.getZ()); - - if (to.getX() < minX || to.getX() > maxX || to.getZ() < minZ || to.getZ() > maxZ) { + 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; } } } - // --- Aktion wenn außerhalb --- if (outside) { Location spawnLocation = getMainSpawnLocation(); - if (spawnLocation != null) { - // Sofortiger Teleport zum definierten Spawn player.teleport(spawnLocation); - - // Feedback an den Spieler player.playSound(player.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 0.5f); player.sendMessage("§8[§6Nexus§8] §cDu hast den Lobby-Bereich verlassen!"); } @@ -109,24 +141,22 @@ public class BorderModule implements Module, Listener { } /** - * Holt den zentralen Spawnpunkt aus der Config (Pfad: spawn.world, spawn.x, etc.) + * 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"), + 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.yaw"), (float) config.getDouble("spawn.pitch")); } } - // Fallback falls kein Spawn gesetzt ist 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/ChickenRain.java b/src/main/java/de/nexuslobby/modules/gadgets/ChickenRain.java index 887df42..8544fa4 100644 --- a/src/main/java/de/nexuslobby/modules/gadgets/ChickenRain.java +++ b/src/main/java/de/nexuslobby/modules/gadgets/ChickenRain.java @@ -14,7 +14,15 @@ import java.util.Random; public class ChickenRain { + // FIX: Verhindert, dass ein Spieler den Regen mehrfach gleichzeitig starten kann. + // Ohne diese Prüfung konnten beliebig viele parallele Tasks gestartet werden, + // was zu hunderten gespawnten Entities in Sekunden führte. + private static final java.util.Set activeRains = + java.util.Collections.synchronizedSet(new java.util.HashSet<>()); + public static void start(Player player) { + if (activeRains.contains(player.getUniqueId())) return; // bereits aktiv + activeRains.add(player.getUniqueId()); new BukkitRunnable() { int ticks = 0; final Random random = new Random(); @@ -22,6 +30,7 @@ public class ChickenRain { @Override public void run() { if (!player.isOnline() || ticks > 100) { // 100 Ticks = 5 Sekunden + activeRains.remove(player.getUniqueId()); this.cancel(); return; } diff --git a/src/main/java/de/nexuslobby/modules/gadgets/FreezeRay.java b/src/main/java/de/nexuslobby/modules/gadgets/FreezeRay.java index d3a284b..8815a66 100644 --- a/src/main/java/de/nexuslobby/modules/gadgets/FreezeRay.java +++ b/src/main/java/de/nexuslobby/modules/gadgets/FreezeRay.java @@ -13,8 +13,14 @@ import java.util.Set; import java.util.UUID; public class FreezeRay { - // Auf public gesetzt, damit das GadgetModule im PlayerMoveEvent darauf prüfen kann - public static final Set frozenPlayers = new HashSet<>(); + + // FIX: private statt public – Zugriff nur über isFrozen() und unfreeze() + private static final Set frozenPlayers = new HashSet<>(); + + public static boolean isFrozen(UUID uuid) { return frozenPlayers.contains(uuid); } + + /** Beim Disconnect aufrufen, damit kein Ghost-Eintrag bleibt. */ + public static void unfreeze(UUID uuid) { frozenPlayers.remove(uuid); } public static void shoot(Player shooter) { Location start = shooter.getEyeLocation(); diff --git a/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java b/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java index 9f6aa20..d0d69f2 100644 --- a/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java +++ b/src/main/java/de/nexuslobby/modules/gadgets/GadgetModule.java @@ -47,6 +47,8 @@ public class GadgetModule implements Module, Listener { @Override public void onEnable() { Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); + // FIX: PetManager-Listener korrekt registrieren (war vorher toter Code) + PetManager.register(); Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), () -> { PetManager.updatePets(); @@ -83,7 +85,7 @@ public class GadgetModule implements Module, Listener { @EventHandler public void onPlayerMove(PlayerMoveEvent event) { - if (FreezeRay.frozenPlayers.contains(event.getPlayer().getUniqueId())) { + if (FreezeRay.isFrozen(event.getPlayer().getUniqueId())) { if (event.getFrom().getX() != event.getTo().getX() || event.getFrom().getZ() != event.getTo().getZ()) { event.setTo(event.getFrom().setDirection(event.getTo().getDirection())); } @@ -295,6 +297,7 @@ public class GadgetModule implements Module, Listener { activeEffects.remove(player.getUniqueId()); activeShields.remove(player.getUniqueId()); PetManager.removePet(player); + FreezeRay.unfreeze(player.getUniqueId()); HatManager.removeHat(player); player.getInventory().remove(Material.FISHING_ROD); player.getInventory().remove(Material.PACKED_ICE); @@ -336,6 +339,8 @@ public class GadgetModule implements Module, Listener { @Override public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); + PetManager.unregister(); PetManager.clearAll(); activeBalloons.values().forEach(Balloon::remove); activeBalloons.clear(); diff --git a/src/main/java/de/nexuslobby/modules/gadgets/PetManager.java b/src/main/java/de/nexuslobby/modules/gadgets/PetManager.java index b501e5d..f440105 100644 --- a/src/main/java/de/nexuslobby/modules/gadgets/PetManager.java +++ b/src/main/java/de/nexuslobby/modules/gadgets/PetManager.java @@ -22,10 +22,30 @@ public class PetManager implements Listener { private static final Map activePets = new HashMap<>(); + // Singleton-Instanz, damit registerEvents() nur einmal aufgerufen wird + private static PetManager instance; + public PetManager() { Bukkit.getPluginManager().registerEvents(this, NexusLobby.getInstance()); } + /** + * Registriert den PetManager als Listener, falls noch nicht geschehen. + * Muss einmalig beim Plugin-Start aufgerufen werden (z.B. aus GadgetModule.onEnable). + */ + public static void register() { + if (instance == null) { + instance = new PetManager(); + } + } + + public static void unregister() { + if (instance != null) { + org.bukkit.event.HandlerList.unregisterAll(instance); + instance = null; + } + } + /** * Spawnt ein echtes Tier-Entity für den Spieler. */ diff --git a/src/main/java/de/nexuslobby/modules/hologram/HologramModule.java b/src/main/java/de/nexuslobby/modules/hologram/HologramModule.java index 5652ec2..726fc24 100644 --- a/src/main/java/de/nexuslobby/modules/hologram/HologramModule.java +++ b/src/main/java/de/nexuslobby/modules/hologram/HologramModule.java @@ -172,6 +172,7 @@ public class HologramModule implements Module, Listener { @Override public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); holograms.values().forEach(NexusHologram::removeAll); holograms.clear(); } diff --git a/src/main/java/de/nexuslobby/modules/intro/IntroModule.java b/src/main/java/de/nexuslobby/modules/intro/IntroModule.java index e598c84..701f65e 100644 --- a/src/main/java/de/nexuslobby/modules/intro/IntroModule.java +++ b/src/main/java/de/nexuslobby/modules/intro/IntroModule.java @@ -49,6 +49,7 @@ public class IntroModule implements Module, Listener, CommandExecutor { @Override public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); activeIntro.clear(); } diff --git a/src/main/java/de/nexuslobby/modules/mapart/MapArtModule.java b/src/main/java/de/nexuslobby/modules/mapart/MapArtModule.java index 415ce5e..d1f8202 100644 --- a/src/main/java/de/nexuslobby/modules/mapart/MapArtModule.java +++ b/src/main/java/de/nexuslobby/modules/mapart/MapArtModule.java @@ -29,6 +29,8 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.net.URL; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -38,8 +40,18 @@ public class MapArtModule implements Module, CommandExecutor { private File storageFile; private FileConfiguration storageConfig; - // RAM-Schutz: Cache für bereits geladene Bild-Kacheln - private final Map tileCache = new ConcurrentHashMap<>(); + // FIX: Unbegrenzter Cache führt bei vielen verschiedenen Bild-URLs zu einem + // OutOfMemoryError. Stattdessen nutzen wir einen LRU-Cache mit max. 50 Einträgen. + // Älteste Kacheln werden automatisch verdrängt. + private static final int MAX_CACHE_SIZE = 50; + private final Map tileCache = Collections.synchronizedMap( + new LinkedHashMap(MAX_CACHE_SIZE + 1, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_CACHE_SIZE; + } + } + ); // Hilfs-Set um parallele Downloads der gleichen URL zu verhindern private final Map loadingShield = new ConcurrentHashMap<>(); diff --git a/src/main/java/de/nexuslobby/modules/player/PlayerInspectModule.java b/src/main/java/de/nexuslobby/modules/player/PlayerInspectModule.java index a52a1ef..4a93140 100644 --- a/src/main/java/de/nexuslobby/modules/player/PlayerInspectModule.java +++ b/src/main/java/de/nexuslobby/modules/player/PlayerInspectModule.java @@ -37,7 +37,9 @@ public class PlayerInspectModule implements Module, Listener { } @Override - public void onDisable() {} + public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); + } @EventHandler public void onPlayerInteract(PlayerInteractEntityEvent event) { diff --git a/src/main/java/de/nexuslobby/modules/portal/PortalCommand.java b/src/main/java/de/nexuslobby/modules/portal/PortalCommand.java index 0596151..8ccde05 100644 --- a/src/main/java/de/nexuslobby/modules/portal/PortalCommand.java +++ b/src/main/java/de/nexuslobby/modules/portal/PortalCommand.java @@ -5,12 +5,15 @@ import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; import java.util.HashMap; import java.util.Map; import java.util.UUID; -public class PortalCommand implements CommandExecutor { +public class PortalCommand implements CommandExecutor, Listener { private final PortalManager portalManager; @@ -20,6 +23,17 @@ public class PortalCommand implements CommandExecutor { public PortalCommand(PortalManager portalManager) { this.portalManager = portalManager; + // FIX: Listener registrieren, damit Selektionen beim Verlassen bereinigt werden + de.nexuslobby.NexusLobby.getInstance().getServer().getPluginManager() + .registerEvents(this, de.nexuslobby.NexusLobby.getInstance()); + } + + // FIX: Selektionen beim Quit entfernen – statische Maps wuchsen sonst ewig + @EventHandler + public void onQuit(PlayerQuitEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + selection1.remove(uuid); + selection2.remove(uuid); } // Statische Hilfsmethoden für andere Klassen diff --git a/src/main/java/de/nexuslobby/modules/portal/PortalManager.java b/src/main/java/de/nexuslobby/modules/portal/PortalManager.java index 76355d8..c9ff6b2 100644 --- a/src/main/java/de/nexuslobby/modules/portal/PortalManager.java +++ b/src/main/java/de/nexuslobby/modules/portal/PortalManager.java @@ -31,7 +31,12 @@ import java.util.UUID; import java.util.Set; /** - * PortalManager - Verwaltet Portale, Markierungen und den globalen Grenzschutz. + * PortalManager - Verwaltet Portale und Wand-Markierungen. + * + * FIX: Die doppelte Border-Logik wurde entfernt. Der BorderModule ist + * der alleinige Verantwortliche für die Lobby-Grenze. Hätte der + * PortalManager die Border ebenfalls geprüft, wären Spieler doppelt + * teleportiert worden und hätten zwei Nachrichten erhalten. */ public class PortalManager implements Module, Listener { @@ -42,12 +47,6 @@ public class PortalManager implements Module, Listener { private final NamespacedKey wandKey; private BukkitTask particleTask; - // Boundary Cache - private Location borderMin; - private Location borderMax; - private boolean borderEnabled = false; - private String borderType; - public PortalManager(NexusLobby plugin) { this.plugin = plugin; this.wandKey = new NamespacedKey(plugin, "nexuslobby_portal_wand"); @@ -61,7 +60,6 @@ public class PortalManager implements Module, Listener { @Override public void onEnable() { loadPortals(); - loadBorderSettings(); Bukkit.getPluginManager().registerEvents(this, plugin); startParticleTask(); } @@ -76,28 +74,6 @@ public class PortalManager implements Module, Listener { plugin.getLogger().info("PortalManager deaktiviert."); } - public void loadBorderSettings() { - this.borderType = plugin.getConfig().getString("worldborder.type", "SQUARE"); - boolean enabled = plugin.getConfig().getBoolean("worldborder.enabled", false); - if (!enabled || !"SQUARE".equalsIgnoreCase(borderType)) { - this.borderEnabled = false; - return; - } - - if (plugin.getConfig().contains("worldborder.pos1") && plugin.getConfig().contains("worldborder.pos2")) { - Location p1 = plugin.getConfig().getLocation("worldborder.pos1"); - Location p2 = plugin.getConfig().getLocation("worldborder.pos2"); - if (p1 != null && p2 != null) { - this.borderMin = getMinLocation(p1, p2); - this.borderMax = getMaxLocation(p1, p2); - this.borderEnabled = true; - return; - } - } - - this.borderEnabled = false; - } - public Set getPortalNames() { return portals.keySet(); } @@ -135,20 +111,20 @@ public class PortalManager implements Module, Listener { if (event.getAction() == Action.LEFT_CLICK_BLOCK) { selectionMap.get(uuid)[0] = clickedLoc; PortalCommand.setSelection1(p, clickedLoc); - + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 2.0f); p.sendMessage("§aPosition 1 gesetzt: " + clickedLoc.getBlockX() + ", " + clickedLoc.getBlockY() + ", " + clickedLoc.getBlockZ()); - + } else if (event.getAction() == Action.RIGHT_CLICK_BLOCK) { selectionMap.get(uuid)[1] = clickedLoc; PortalCommand.setSelection2(p, clickedLoc); - + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 1.0f); p.sendMessage("§bPosition 2 gesetzt: " + clickedLoc.getBlockX() + ", " + clickedLoc.getBlockY() + ", " + clickedLoc.getBlockZ()); Location loc1 = selectionMap.get(uuid)[0]; if (loc1 != null) { - int width = Math.abs(loc1.getBlockX() - clickedLoc.getBlockX()) + 1; + int width = Math.abs(loc1.getBlockX() - clickedLoc.getBlockX()) + 1; int height = Math.abs(loc1.getBlockY() - clickedLoc.getBlockY()) + 1; int length = Math.abs(loc1.getBlockZ() - clickedLoc.getBlockZ()) + 1; long volume = (long) width * height * length; @@ -156,7 +132,7 @@ public class PortalManager implements Module, Listener { p.sendMessage("§7§m----------------------------------"); if (volume < 500) { p.sendMessage("§e[Nexus] Kleiner Bereich erkannt (Portal-Größe)"); - p.sendMessage("§fBefehl: §b/portal create "); + p.sendMessage("§fBefehl: §b/portal create "); } else { p.sendMessage("§6[Nexus] Großer Bereich erkannt (WorldBorder-Größe)"); p.sendMessage("§fBefehl: §a/border square"); @@ -190,12 +166,9 @@ public class PortalManager implements Module, Listener { String type = portalConfig.getString("portals." + key + ".type", "WORLD"); Portal portal = new Portal(key, type); - if (portalConfig.contains("portals." + key + ".pos1")) { - portal.setPos1(portalConfig.getLocation("portals." + key + ".pos1")); - } - if (portalConfig.contains("portals." + key + ".pos2")) { - portal.setPos2(portalConfig.getLocation("portals." + key + ".pos2")); - } + portal.setPos1(loadLocation(portalConfig, "portals." + key + ".pos1")); + portal.setPos2(loadLocation(portalConfig, "portals." + key + ".pos2")); + portal.setReturnSpawn(loadLocation(portalConfig, "portals." + key + ".returnSpawn")); portal.setDestination(portalConfig.getString("portals." + key + ".destination", "")); try { @@ -204,10 +177,6 @@ public class PortalManager implements Module, Listener { portal.setParticle(Particle.PORTAL); } - if (portalConfig.contains("portals." + key + ".returnSpawn")) { - portal.setReturnSpawn(portalConfig.getLocation("portals." + key + ".returnSpawn")); - } - portals.put(key, portal); } } @@ -219,11 +188,11 @@ public class PortalManager implements Module, Listener { for (Portal portal : portals.values()) { String path = "portals." + portal.getName() + "."; config.set(path + "type", portal.getType()); - config.set(path + "pos1", portal.getPos1()); - config.set(path + "pos2", portal.getPos2()); + saveLocation(config, path + "pos1", portal.getPos1()); + saveLocation(config, path + "pos2", portal.getPos2()); config.set(path + "destination", portal.getDestination()); config.set(path + "particle", portal.getParticle() != null ? portal.getParticle().name() : "PORTAL"); - config.set(path + "returnSpawn", portal.getReturnSpawn()); + saveLocation(config, path + "returnSpawn", portal.getReturnSpawn()); } try { @@ -233,6 +202,35 @@ public class PortalManager implements Module, Listener { } } + // FIX: Hilfsmethoden für robustes Location-Speichern/Laden. + // Speichert world + x/y/z als einzelne Keys statt als serialisiertes + // Location-Objekt, was nach Neustarts zu null führen kann. + private void saveLocation(YamlConfiguration config, String path, Location loc) { + if (loc == null || loc.getWorld() == null) return; + config.set(path + ".world", loc.getWorld().getName()); + config.set(path + ".x", loc.getX()); + config.set(path + ".y", loc.getY()); + config.set(path + ".z", loc.getZ()); + config.set(path + ".yaw", (double) loc.getYaw()); + config.set(path + ".pitch", (double) loc.getPitch()); + } + + private Location loadLocation(YamlConfiguration config, String path) { + if (!config.contains(path + ".world")) return null; + String worldName = config.getString(path + ".world"); + World world = Bukkit.getWorld(worldName); + if (world == null) { + plugin.getLogger().warning("Welt '" + worldName + "' nicht gefunden für Pfad: " + path); + return null; + } + return new Location(world, + config.getDouble(path + ".x"), + config.getDouble(path + ".y"), + config.getDouble(path + ".z"), + (float) config.getDouble(path + ".yaw"), + (float) config.getDouble(path + ".pitch")); + } + // --- CRUD --- public boolean createPortal(String name, String type, Player creator) { Location[] sel = selectionMap.getOrDefault(creator.getUniqueId(), new Location[2]); @@ -295,7 +293,7 @@ public class PortalManager implements Module, Listener { return true; } - // --- Movement / Teleport / Boundary Logic --- + // --- Movement / Teleport Logic --- @org.bukkit.event.EventHandler public void onPlayerMove(PlayerMoveEvent event) { if (event.getFrom().getX() == event.getTo().getX() && @@ -307,20 +305,7 @@ public class PortalManager implements Module, Listener { Player player = event.getPlayer(); Location loc = event.getTo(); - // 1. Grenzschutz (Boundary Protection) - if (borderEnabled && !player.hasPermission("nexuslobby.border.bypass")) { - if (!isWithinBorder(loc)) { - Location spawn = getMainSpawnLocation(); - if (spawn != null) { - player.teleport(spawn); - player.playSound(player.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 0.5f); - player.sendMessage("§c§lHEY! §7Du hast den erlaubten Bereich verlassen."); - } - return; - } - } - - // 2. Portal Logik + // Portal-Logik for (Portal portal : portals.values()) { if (portal.getPos1() == null || portal.getPos2() == null) continue; if (!isInArea(loc, portal.getPos1(), portal.getPos2())) continue; @@ -336,14 +321,6 @@ public class PortalManager implements Module, Listener { } } - private boolean isWithinBorder(Location loc) { - if (borderMin == null || borderMax == null) return true; - if (!loc.getWorld().equals(borderMin.getWorld())) return true; - return loc.getX() >= borderMin.getX() && loc.getX() <= borderMax.getX() && - loc.getY() >= borderMin.getY() && loc.getY() <= borderMax.getY() && - loc.getZ() >= borderMin.getZ() && loc.getZ() <= borderMax.getZ(); - } - private void executeTeleport(Player player, Portal portal) { if ("SERVER".equalsIgnoreCase(portal.getType())) { String serverName = portal.getDestination(); @@ -352,17 +329,17 @@ public class PortalManager implements Module, Listener { Location loc = portal.getReturnSpawn(); if (loc == null) { Location pos2 = portal.getPos2(); - loc = player.getLocation(); + loc = player.getLocation().clone(); if (pos2 != null) { double dx = loc.getX() - pos2.getX(); double dz = loc.getZ() - pos2.getZ(); - double length = Math.sqrt(dx*dx + dz*dz); + double length = Math.sqrt(dx * dx + dz * dz); if (length == 0) length = 1; dx = (dx / length) * 2; dz = (dz / length) * 2; loc.add(dx, 0, dz); } else { - loc.add(0,0,2); + loc.add(0, 0, 2); } } player.teleport(loc); @@ -393,7 +370,7 @@ public class PortalManager implements Module, Listener { double x = Double.parseDouble(parts[1]); double y = Double.parseDouble(parts[2]); double z = Double.parseDouble(parts[3]); - float yaw = parts.length >= 6 ? Float.parseFloat(parts[4]) : 0f; + float yaw = parts.length >= 5 ? Float.parseFloat(parts[4]) : 0f; float pitch = parts.length >= 6 ? Float.parseFloat(parts[5]) : 0f; player.teleport(new Location(world, x, y, z, yaw, pitch)); player.sendMessage("§aDu wurdest teleportiert!"); @@ -418,11 +395,11 @@ public class PortalManager implements Module, Listener { if (worldName != null) { World w = Bukkit.getWorld(worldName); if (w != null) { - return new Location(w, - plugin.getConfig().getDouble("spawn.x"), - plugin.getConfig().getDouble("spawn.y"), + return new Location(w, + plugin.getConfig().getDouble("spawn.x"), + plugin.getConfig().getDouble("spawn.y"), plugin.getConfig().getDouble("spawn.z"), - (float) plugin.getConfig().getDouble("spawn.yaw"), + (float) plugin.getConfig().getDouble("spawn.yaw"), (float) plugin.getConfig().getDouble("spawn.pitch")); } } @@ -461,6 +438,7 @@ public class PortalManager implements Module, Listener { Location min = getMinLocation(portal.getPos1(), portal.getPos2()); Location max = getMaxLocation(portal.getPos1(), portal.getPos2()); World world = portal.getPos1().getWorld(); + if (world == null) return; for (int i = 0; i < 15; i++) { double x = min.getX() + Math.random() * (max.getX() - min.getX() + 1); double y = min.getY() + Math.random() * (max.getY() - min.getY() + 1); diff --git a/src/main/java/de/nexuslobby/modules/security/SecurityModule.java b/src/main/java/de/nexuslobby/modules/security/SecurityModule.java index 96d7345..ec12a9a 100644 --- a/src/main/java/de/nexuslobby/modules/security/SecurityModule.java +++ b/src/main/java/de/nexuslobby/modules/security/SecurityModule.java @@ -115,5 +115,7 @@ public class SecurityModule implements Module, Listener { } @Override - public void onDisable() {} + public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/settings/LobbySettingsModule.java b/src/main/java/de/nexuslobby/modules/settings/LobbySettingsModule.java index 01b1055..1db7bd1 100644 --- a/src/main/java/de/nexuslobby/modules/settings/LobbySettingsModule.java +++ b/src/main/java/de/nexuslobby/modules/settings/LobbySettingsModule.java @@ -231,5 +231,8 @@ public class LobbySettingsModule implements Module, Listener { } @Override - public void onDisable() { saveSettings(); } + public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); + saveSettings(); + } } \ No newline at end of file diff --git a/src/main/java/de/nexuslobby/modules/suppressor/GlobalChatSuppressor.java b/src/main/java/de/nexuslobby/modules/suppressor/GlobalChatSuppressor.java index 880e776..009d5ca 100644 --- a/src/main/java/de/nexuslobby/modules/suppressor/GlobalChatSuppressor.java +++ b/src/main/java/de/nexuslobby/modules/suppressor/GlobalChatSuppressor.java @@ -47,6 +47,7 @@ public class GlobalChatSuppressor implements Module, PluginMessageListener, List @Override public void onDisable() { + org.bukkit.event.HandlerList.unregisterAll(this); plugin.getServer().getMessenger().unregisterIncomingPluginChannel(plugin, CHANNEL_CONTROL); plugin.getServer().getMessenger().unregisterIncomingPluginChannel(plugin, CHANNEL_CHAT); plugin.getServer().getMessenger().unregisterOutgoingPluginChannel(plugin, CHANNEL_CONTROL); @@ -105,4 +106,4 @@ public class GlobalChatSuppressor implements Module, PluginMessageListener, List SuppressManager.unsuppress(uuid); } } -} +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index b0a79ea..c4f840f 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -65,12 +65,12 @@ worldborder: # Zentrum der Weltgrenze (optional, kann leer bleiben) # Wenn nicht gesetzt, wird der Spawn-Punkt als Zentrum verwendet - center: + # center wird automatisch über /border circle gesetzt # Alternative: Definiere Eckpunkte (für rechteckige Border) # Format: x,y,z - pos1: - pos2: + # pos1 wird automatisch über /border square gesetzt + # pos2 wird automatisch über /border square gesetzt # ══════════════════════════════════════════════════════════════════════════════ # LOBBY EINSTELLUNGEN @@ -162,7 +162,7 @@ items: # Kompass (Server-Auswahl / Teleporter) compass: # Aktiviert das Kompass-Item (true = an, false = aus) - enabled: true + enabled: false # Anzeigename des Items (unterstützt Farbcodes) displayname: "&eTeleporter" @@ -184,7 +184,7 @@ items: # Gadget-Menü (Spezialeffekte und Extras) gadget: # Aktiviert das Gadget-Item (true = an, false = aus) - enabled: false + enabled: true # Anzeigename des Items (unterstützt Farbcodes) displayname: "&bGadgets" @@ -283,7 +283,7 @@ player_inspect: # Join/Quit-Nachrichten Unterdrückung und BungeeCord-Messaging suppressor: # Aktiviert das Suppressor-System (true = an, false = aus) - enabled: true + enabled: false # Unterdrückt Join- und Quit-Nachrichten für neue Spieler temporär # true = Nachrichten werden unterdrückt, false = normale Anzeige @@ -402,7 +402,7 @@ ball: enabled: true # Spawn-Position des Balls - # Tipp: Nutze /nexus ball setspawn um diese Position automatisch zu setzen + # Tipp: Nutze /nexuslobby ball setspawn um diese Position automatisch zu setzen spawn: # Name der Welt in der der Ball spawnt world: "world" @@ -425,6 +425,57 @@ ball: # 0 = Kein automatischer Respawn respawn_delay: 60 + # ══════════════════════════════════════════════════════════════════════════ + # TOR-DEFINITIONEN + # ══════════════════════════════════════════════════════════════════════════ + # Tore werden automatisch per Befehl gesetzt und hier gespeichert. + # Manuell bearbeiten ist möglich, aber der Befehlsweg ist empfohlen. + # + # Anleitung zum Einrichten eines Tores: + # 1. Gehe zur INNEREN Ecke des Torrahmens (z.B. linke untere Ecke) + # /nexuslobby ball goal pos1 + # 2. Gehe zur GEGENÜBERLIEGENDEN Ecke (rechte obere Ecke, auch 1-2 Blöcke + # hinter dem Tor in Schussrichtung für bessere Erkennung!) + # /nexuslobby ball goal pos2 + # 3. Speichern (Team 1 = Blau, Team 2 = Rot): + # /nexuslobby ball goal save torBlau 1 + # + # WICHTIG für zuverlässige Erkennung: + # - Die Box muss MINDESTENS 1.5 Blöcke Tiefe in Schussrichtung haben + # - Die Box muss MINDESTENS 3 Blöcke hoch sein (Y-Achse) + # - Nutze /nexuslobby ball goal show um die Box sichtbar zu machen + # - Nutze /nexuslobby ball goal debug um die Ball-Position live zu prüfen + # + # Beispiel-Eintrag (wird vom Plugin automatisch befüllt): + # goals: + # torBlau: + # pos1: + # world: world + # x: 100.0 + # y: 64.0 + # z: 198.0 + # pos2: + # world: world + # x: 106.0 + # y: 68.0 + # z: 200.5 # <-- 2.5 Blöcke Tiefe in Z-Richtung! + # team: 1 + # torRot: + # pos1: + # world: world + # x: 100.0 + # y: 64.0 + # z: -198.0 + # pos2: + # world: world + # x: 106.0 + # y: 68.0 + # z: -200.5 + # team: 2 + # + # Tore werden hier automatisch eingetragen sobald du den save-Befehl nutzt: + goals: {} + # ══════════════════════════════════════════════════════════════════════════════ # Links # ══════════════════════════════════════════════════════════════════════════════ diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index b7ae426..1f473a9 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: NexusLobby main: de.nexuslobby.NexusLobby -version: "1.1.1" +version: "1.1.3" api-version: "1.21" author: M_Viper description: Modular Lobby Plugin with an invisible Particle-Parkour system.