397 lines
14 KiB
Java
397 lines
14 KiB
Java
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;
|
|
}
|
|
}
|
|
} |