package de.nexuslobby.modules.ball; import de.nexuslobby.NexusLobby; import de.nexuslobby.api.Module; import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.player.PlayerInteractAtEntityEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.profile.PlayerProfile; import org.bukkit.util.Vector; import java.net.MalformedURLException; import java.net.URL; import java.util.UUID; import java.util.Objects; 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"; 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"; } @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(); // 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 // Haupt-Physik & Anti-Duplikat System Bukkit.getScheduler().runTaskTimer(NexusLobby.getInstance(), () -> { // Anti-Duplikat-Check (optimiert: nur in Ball-Welt) if (ball != null && ball.isValid()) { removeDuplicateBalls(); } if (ball == null || !ball.isValid()) return; 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) { respawnBall(); } }, 1L, 1L); } /** * Optimierte Dribbel-Logik: Ball folgt nahen Spielern */ 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)); 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(); 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 (bounced) { ball.setVelocity(vel); ball.getWorld().playSound(ball.getLocation(), 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); } } private void spawnBall() { if (spawnLocation == null || spawnLocation.getWorld() == null) { // Keine Warnung mehr in der Konsole ausgeben 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); } lastMoveTime = System.currentTimeMillis(); } private ItemStack getSoccerHead() { ItemStack head = new ItemStack(Material.PLAYER_HEAD); SkullMeta meta = (SkullMeta) head.getItemMeta(); if (meta == null) return head; PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID(), BALL_NAME); try { profile.getTextures().setSkin(new URL(TEXTURE_URL)); } catch (MalformedURLException e) { Bukkit.getLogger().warning("[NexusLobby] Ungültige Ball-Textur URL!"); } meta.setOwnerProfile(profile); head.setItemMeta(meta); return head; } public void respawnBall() { if (ball != null && ball.isValid()) { ball.remove(); ball = null; } removeAllOldBalls(); spawnBall(); } @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(); } } } @EventHandler public void onBallInteract(PlayerInteractAtEntityEvent event) { 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"); } } @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 (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; } } } return false; } @Override public void onDisable() { if (ball != null && ball.isValid()) { ball.remove(); ball = null; } } }