Update from Git Manager GUI
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package de.fussball.plugin.game;
|
||||
|
||||
import de.fussball.plugin.Fussball;
|
||||
import de.fussball.plugin.utils.MessageUtil;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.entity.*;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
@@ -9,6 +8,7 @@ import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
import org.bukkit.profile.PlayerProfile;
|
||||
import org.bukkit.profile.PlayerTextures;
|
||||
import org.bukkit.util.EulerAngle;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
@@ -20,10 +20,21 @@ public class Ball {
|
||||
private static final String BALL_TEXTURE_URL =
|
||||
"https://textures.minecraft.net/texture/451f8cfcfb85d77945dc6a3618414093e70436b46d2577b28c727f1329b7265e";
|
||||
|
||||
// Reibung: jeder Tick wird horizontale Geschwindigkeit um diesen Faktor reduziert
|
||||
private static final double FRICTION = 0.82;
|
||||
// Unter dieser Geschwindigkeit → Ball stoppt komplett
|
||||
private static final double MIN_VELOCITY = 0.04;
|
||||
// ── Physik-Konstanten ─────────────────────────────────────────────────────
|
||||
/** Reibung am Boden (pro Tick) – simuliert Rasen */
|
||||
private static final double FRICTION_GROUND = 0.76;
|
||||
/** Luftwiderstand (pro Tick) – deutlich weniger als Bodenreibung */
|
||||
private static final double FRICTION_AIR = 0.985;
|
||||
/** Abprall-Koeffizient: 0 = kein Abprall, 1 = perfekter Abprall */
|
||||
private static final double BOUNCE_RESTITUTION = 0.42;
|
||||
/** Minimale aufprallY-Geschwindigkeit für sichtbaren Abprall */
|
||||
private static final double MIN_BOUNCE_VEL = 0.12;
|
||||
/** Unter dieser Horizontalgeschwindigkeit stoppt der Ball */
|
||||
private static final double MIN_VELOCITY = 0.025;
|
||||
/** Maximale Ballgeschwindigkeit (verhindert unrealistisch schnelle Bälle) */
|
||||
private static final double MAX_VELOCITY = 3.8;
|
||||
/** Anteil der Blickrichtung beim normalen Schuss (0=nur Ballrichtung, 1=nur Blick) */
|
||||
private static final double LOOK_BLEND = 0.45;
|
||||
|
||||
private final Game game;
|
||||
private final Fussball plugin;
|
||||
@@ -31,30 +42,37 @@ public class Ball {
|
||||
private final Location spawnLocation;
|
||||
private boolean active = false;
|
||||
|
||||
// ── Torwart-Halten ───────────────────────────────────────────────────────
|
||||
// ── Torwart-Halten ────────────────────────────────────────────────────────
|
||||
private boolean heldByGoalkeeper = false;
|
||||
private Player holdingPlayer = null;
|
||||
|
||||
// ── Physik-State ──────────────────────────────────────────────────────────
|
||||
/** Y-Geschwindigkeit des letzten Ticks – für Abprall-Erkennung */
|
||||
private double prevYVelocity = 0.0;
|
||||
/** Akkumulierter Rollwinkel für visuelle Rotation (Radianten) */
|
||||
private double rollAngle = 0.0;
|
||||
/** Ticks bis nächstes Rollgeräusch */
|
||||
private int rollSoundTick = 0;
|
||||
|
||||
public Ball(Game game, Fussball plugin, Location spawnLocation) {
|
||||
this.game = game;
|
||||
this.plugin = plugin;
|
||||
this.game = game;
|
||||
this.plugin = plugin;
|
||||
this.spawnLocation = spawnLocation.clone();
|
||||
}
|
||||
|
||||
// ── Config-Helfer ────────────────────────────────────────────────────────
|
||||
// ── Config-Helfer ─────────────────────────────────────────────────────────
|
||||
|
||||
private double cfg(String path, double def) {
|
||||
return plugin.getConfig().getDouble(path, def);
|
||||
}
|
||||
|
||||
// ── Spawnen ──────────────────────────────────────────────────────────────
|
||||
// ── Spawnen ───────────────────────────────────────────────────────────────
|
||||
|
||||
public void spawn() {
|
||||
if (spawnLocation == null || spawnLocation.getWorld() == null) {
|
||||
plugin.getLogger().severe("[Fussball] Ball-Spawn Location ist null!"); return;
|
||||
}
|
||||
if (entity != null && !entity.isDead()) entity.remove();
|
||||
|
||||
spawnLocation.getWorld().loadChunk(spawnLocation.getBlockX() >> 4, spawnLocation.getBlockZ() >> 4);
|
||||
|
||||
entity = spawnLocation.getWorld().spawn(
|
||||
@@ -78,16 +96,18 @@ public class Ball {
|
||||
if (entity == null) {
|
||||
plugin.getLogger().severe("[Fussball] ArmorStand konnte nicht gespawnt werden!"); return;
|
||||
}
|
||||
active = true;
|
||||
active = true;
|
||||
prevYVelocity = 0.0;
|
||||
rollAngle = 0.0;
|
||||
}
|
||||
|
||||
private ItemStack createBallItem() {
|
||||
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
||||
SkullMeta meta = (SkullMeta) skull.getItemMeta();
|
||||
if (meta == null) return skull;
|
||||
meta.setDisplayName("§e⚽ Fußball");
|
||||
try {
|
||||
PlayerProfile profile = Bukkit.createPlayerProfile(
|
||||
PlayerProfile profile = Bukkit.createPlayerProfile(
|
||||
UUID.nameUUIDFromBytes("FussballBall".getBytes()), "FussballBall");
|
||||
PlayerTextures textures = profile.getTextures();
|
||||
textures.setSkin(new URL(BALL_TEXTURE_URL));
|
||||
@@ -100,17 +120,34 @@ public class Ball {
|
||||
return skull;
|
||||
}
|
||||
|
||||
// ── Schuss ───────────────────────────────────────────────────────────────
|
||||
// ── Schüsse ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Normaler Schuss (Rechtsklick) */
|
||||
/**
|
||||
* Kopfball – flacherer Winkel, stärkere Blickrichtungs-Gewichtung.
|
||||
*/
|
||||
public void header(Player player) {
|
||||
if (entity == null || entity.isDead() || !active) return;
|
||||
Location ballLoc = entity.getLocation();
|
||||
Vector dir = blendDirection(player, 0.65);
|
||||
double power = cfg("ball.header-power", 1.3);
|
||||
dir.setY(Math.max(dir.getY(), -0.05));
|
||||
dir.multiply(power);
|
||||
applyKick(dir, ballLoc, 0.85f);
|
||||
ballLoc.getWorld().spawnParticle(Particle.POOF, ballLoc, 4, 0.1, 0.1, 0.1, 0.02);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaler Schuss (Rechtsklick / Nähe).
|
||||
* Mischt Ball→Spieler-Richtung mit Blickrichtung.
|
||||
*/
|
||||
public void kick(Player player) {
|
||||
if (entity == null || entity.isDead() || !active) return;
|
||||
Location ballLoc = entity.getLocation();
|
||||
Vector dir = getKickDirection(player);
|
||||
Vector dir = blendDirection(player, LOOK_BLEND);
|
||||
|
||||
double kickVertical = cfg("ball.kick-vertical", 0.3);
|
||||
double kickPower = cfg("ball.kick-power", 1.1);
|
||||
double sprintKickPower = cfg("ball.sprint-kick-power", 1.8);
|
||||
double kickVertical = cfg("ball.kick-vertical", 0.3);
|
||||
double kickPower = cfg("ball.kick-power", 1.1);
|
||||
double sprintKickPower = cfg("ball.sprint-kick-power", 1.8);
|
||||
|
||||
dir.setY(kickVertical);
|
||||
dir.multiply(player.isSprinting() ? sprintKickPower : kickPower);
|
||||
@@ -118,79 +155,204 @@ public class Ball {
|
||||
}
|
||||
|
||||
/**
|
||||
* Aufgeladener Schuss (Shift gedrückt halten → loslassen)
|
||||
* power = 0.0 (kurz gehalten) bis 1.0 (voll aufgeladen, ~1.5s)
|
||||
* Aufgeladener Schuss (Shift halten → loslassen).
|
||||
* power = 0.0 bis 1.0 → beeinflusst Kraft und Loft.
|
||||
*/
|
||||
public void chargedKick(Player player, double power) {
|
||||
if (entity == null || entity.isDead() || !active) return;
|
||||
Location ballLoc = entity.getLocation();
|
||||
Vector dir = getKickDirection(player);
|
||||
// Bei vollem Schuss folgt der Ball mehr der Blickrichtung
|
||||
Vector dir = blendDirection(player, LOOK_BLEND + power * 0.2);
|
||||
|
||||
double minPower = cfg("ball.charged-min-power", 1.3);
|
||||
double maxPower = cfg("ball.charged-max-power", 3.8);
|
||||
double actualMultiplier = minPower + (maxPower - minPower) * power;
|
||||
double mult = minPower + (maxPower - minPower) * power;
|
||||
|
||||
dir.setY(0.25 + power * 0.25); // mehr Loft bei vollem Schuss
|
||||
dir.multiply(actualMultiplier);
|
||||
float pitch = 1.0f + (float) (power * 0.8f);
|
||||
dir.setY(0.22 + power * 0.28);
|
||||
dir.multiply(mult);
|
||||
float pitch = 0.9f + (float)(power * 0.9f);
|
||||
applyKick(dir, ballLoc, pitch);
|
||||
|
||||
// Feuer-Partikel ab 70% Ladung
|
||||
if (power > 0.7) {
|
||||
ballLoc.getWorld().spawnParticle(Particle.FLAME, ballLoc, 12, 0.2, 0.2, 0.2, 0.06);
|
||||
ballLoc.getWorld().spawnParticle(Particle.FLAME, ballLoc, 14, 0.2, 0.2, 0.2, 0.07);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector getKickDirection(Player player) {
|
||||
Vector dir = entity.getLocation().toVector().subtract(player.getLocation().toVector());
|
||||
if (dir.lengthSquared() < 0.0001) dir = player.getLocation().getDirection();
|
||||
dir.normalize();
|
||||
return dir;
|
||||
/**
|
||||
* Mischt Spieler→Ball-Richtung mit der horizontalen Blickrichtung.
|
||||
* Ergibt natürlicheres Schussverhalten als nur die Ball-Richtung.
|
||||
*
|
||||
* @param lookBlend 0.0 = rein Ball-Richtung, 1.0 = rein Blickrichtung
|
||||
*/
|
||||
private Vector blendDirection(Player player, double lookBlend) {
|
||||
Vector toBall = entity.getLocation().toVector()
|
||||
.subtract(player.getLocation().toVector())
|
||||
.setY(0);
|
||||
if (toBall.lengthSquared() < 0.0001) toBall = player.getLocation().getDirection().setY(0);
|
||||
toBall.normalize();
|
||||
|
||||
Vector look = player.getLocation().getDirection().clone().setY(0);
|
||||
if (look.lengthSquared() < 0.0001) look = toBall.clone();
|
||||
else look.normalize();
|
||||
|
||||
Vector result = toBall.clone().multiply(1.0 - lookBlend).add(look.multiply(lookBlend));
|
||||
if (result.lengthSquared() < 0.0001) result = toBall;
|
||||
return result.normalize();
|
||||
}
|
||||
|
||||
private void applyKick(Vector dir, Location ballLoc, float soundPitch) {
|
||||
// Geschwindigkeitsbegrenzung
|
||||
if (dir.length() > MAX_VELOCITY) dir.normalize().multiply(MAX_VELOCITY);
|
||||
entity.setVelocity(dir);
|
||||
prevYVelocity = dir.getY();
|
||||
ballLoc.getWorld().playSound(ballLoc, Sound.ENTITY_GENERIC_SMALL_FALL, 1.0f, soundPitch);
|
||||
ballLoc.getWorld().spawnParticle(Particle.POOF, ballLoc, 5, 0.1, 0.1, 0.1, 0.05);
|
||||
}
|
||||
|
||||
// ── Physik ───────────────────────────────────────────────────────────────
|
||||
|
||||
public void applyFriction() {
|
||||
if (heldByGoalkeeper) return; // Kein Reibung wenn der TW den Ball hält
|
||||
if (entity == null || entity.isDead() || !active) return;
|
||||
Vector vel = entity.getVelocity();
|
||||
double hx = vel.getX() * FRICTION;
|
||||
double hz = vel.getZ() * FRICTION;
|
||||
if (Math.abs(hx) < MIN_VELOCITY) hx = 0;
|
||||
if (Math.abs(hz) < MIN_VELOCITY) hz = 0;
|
||||
entity.setVelocity(new Vector(hx, vel.getY(), hz));
|
||||
}
|
||||
|
||||
// ── Torwart-Mechanik ─────────────────────────────────────────────────────
|
||||
// ── Physik (jeden Tick) ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Torwart greift den Ball – Ball schwebt vor ihm und folgt ihm.
|
||||
* Prüft vorher, ob er im erlaubten Bereich ist.
|
||||
* Haupt-Physik-Update – jeden Tick vom GoalCheckLoop aufgerufen (ersetzt applyFriction).
|
||||
* Verarbeitet: Reibung (Luft/Boden), Abprall, Wandreflektion, visuelle Rotation.
|
||||
*/
|
||||
public void holdBall(Player goalkeeper) {
|
||||
public void applyPhysics() {
|
||||
if (heldByGoalkeeper) return;
|
||||
if (entity == null || entity.isDead() || !active) return;
|
||||
|
||||
// NEUE REGEL: Prüfen, ob Spieler im 2-Block-Radius am Tor ist
|
||||
if (!game.isAllowedToHoldBall(goalkeeper)) {
|
||||
goalkeeper.sendMessage(MessageUtil.error("Du kannst den Ball nur innerhalb von 2 Blöcken am Tor halten!"));
|
||||
return;
|
||||
Vector vel = entity.getVelocity();
|
||||
boolean onGround = entity.isOnGround();
|
||||
|
||||
// ── 1. Aufprall-Abprall ───────────────────────────────────────────────
|
||||
if (onGround && prevYVelocity < -MIN_BOUNCE_VEL) {
|
||||
double bounceY = -prevYVelocity * BOUNCE_RESTITUTION;
|
||||
if (bounceY > MIN_BOUNCE_VEL) {
|
||||
vel.setY(bounceY);
|
||||
entity.setVelocity(vel);
|
||||
// Sound proportional zur Aufprallstärke
|
||||
float vol = (float) Math.min(1.0, Math.abs(prevYVelocity) * 0.65);
|
||||
float tone = 0.7f + (float)(Math.abs(prevYVelocity) * 0.25);
|
||||
entity.getLocation().getWorld().playSound(
|
||||
entity.getLocation(), Sound.BLOCK_GRASS_HIT, vol, tone);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. Reibung – Boden vs. Luft ──────────────────────────────────────
|
||||
double friction = onGround ? FRICTION_GROUND : FRICTION_AIR;
|
||||
double hx = vel.getX() * friction;
|
||||
double hz = vel.getZ() * friction;
|
||||
if (Math.abs(hx) < MIN_VELOCITY) hx = 0;
|
||||
if (Math.abs(hz) < MIN_VELOCITY) hz = 0;
|
||||
|
||||
// ── 3. Wandabprall ────────────────────────────────────────────────────
|
||||
if (!onGround) {
|
||||
Vector reflected = checkWallCollision(vel);
|
||||
if (reflected != null) {
|
||||
hx = reflected.getX() * 0.58;
|
||||
hz = reflected.getZ() * 0.58;
|
||||
}
|
||||
}
|
||||
|
||||
vel.setX(hx);
|
||||
vel.setZ(hz);
|
||||
entity.setVelocity(vel);
|
||||
prevYVelocity = vel.getY();
|
||||
|
||||
// ── 4. Visuelle Rotation ──────────────────────────────────────────────
|
||||
updateVisualRotation(hx, hz, onGround);
|
||||
|
||||
// ── 5. Rollgeräusch ───────────────────────────────────────────────────
|
||||
if (rollSoundTick > 0) {
|
||||
rollSoundTick--;
|
||||
} else if (onGround) {
|
||||
double speed = Math.sqrt(hx * hx + hz * hz);
|
||||
if (speed > 0.12) {
|
||||
float vol = (float) Math.min(0.3, speed * 0.18);
|
||||
float tone = 0.75f + (float)(speed * 0.18);
|
||||
entity.getLocation().getWorld().playSound(
|
||||
entity.getLocation(), Sound.BLOCK_GRASS_STEP, vol, tone);
|
||||
rollSoundTick = Math.max(2, (int)(9 - speed * 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob der Ball in Bewegungsrichtung gegen einen festen Block läuft.
|
||||
* @return Reflektierter Geschwindigkeitsvektor, oder null wenn kein Hindernis.
|
||||
*/
|
||||
private Vector checkWallCollision(Vector vel) {
|
||||
if (vel.lengthSquared() < 0.01) return null;
|
||||
Location loc = entity.getLocation().add(0, 0.25, 0);
|
||||
World world = loc.getWorld();
|
||||
if (world == null) return null;
|
||||
|
||||
double check = 0.42;
|
||||
boolean hitX = false, hitZ = false;
|
||||
|
||||
if (Math.abs(vel.getX()) > 0.04) {
|
||||
if (loc.clone().add(Math.signum(vel.getX()) * check, 0, 0)
|
||||
.getBlock().getType().isSolid()) hitX = true;
|
||||
}
|
||||
if (Math.abs(vel.getZ()) > 0.04) {
|
||||
if (loc.clone().add(0, 0, Math.signum(vel.getZ()) * check)
|
||||
.getBlock().getType().isSolid()) hitZ = true;
|
||||
}
|
||||
if (!hitX && !hitZ) return null;
|
||||
|
||||
Vector reflected = vel.clone();
|
||||
if (hitX) reflected.setX(-vel.getX());
|
||||
if (hitZ) reflected.setZ(-vel.getZ());
|
||||
|
||||
world.playSound(loc, Sound.BLOCK_STONE_HIT, 0.45f, 1.15f);
|
||||
world.spawnParticle(Particle.POOF, loc, 2, 0.05, 0.05, 0.05, 0.01);
|
||||
return reflected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dreht den ArmorStand-Kopf in Bewegungsrichtung – simuliert einen rollenden Ball.
|
||||
* Der Rollwinkel akkumuliert sich mit der Geschwindigkeit.
|
||||
*/
|
||||
private void updateVisualRotation(double vx, double vz, boolean onGround) {
|
||||
if (entity == null || entity.isDead()) return;
|
||||
double speed = Math.sqrt(vx * vx + vz * vz);
|
||||
if (speed < 0.015) return;
|
||||
|
||||
// Rollwinkel akkumulieren (ca. 1.4 Radianten pro Block)
|
||||
rollAngle += speed * 1.4;
|
||||
|
||||
// Yaw = Bewegungsrichtung (Rollachse)
|
||||
double yaw = Math.atan2(-vx, vz);
|
||||
|
||||
if (onGround) {
|
||||
// Am Boden: realistisches Rollverhalten
|
||||
entity.setHeadPose(new EulerAngle(
|
||||
Math.sin(rollAngle) * 0.9, // Nicken (vorwärts/rückwärts)
|
||||
yaw, // Rollrichtung
|
||||
Math.cos(rollAngle) * 0.35 // leichtes Kippen
|
||||
));
|
||||
} else {
|
||||
// In der Luft: Ball dreht sich frei
|
||||
entity.setHeadPose(new EulerAngle(
|
||||
rollAngle % (2 * Math.PI),
|
||||
yaw,
|
||||
(rollAngle * 0.3) % (2 * Math.PI)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Torwart-Mechanik ──────────────────────────────────────────────────────
|
||||
|
||||
public boolean holdBall(Player goalkeeper) {
|
||||
if (entity == null || entity.isDead() || !active) return false;
|
||||
if (!game.isAllowedToHoldBall(goalkeeper)) return false;
|
||||
|
||||
this.heldByGoalkeeper = true;
|
||||
this.holdingPlayer = goalkeeper;
|
||||
entity.setGravity(false);
|
||||
entity.setVelocity(new Vector(0, 0, 0));
|
||||
prevYVelocity = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Muss jeden Tick aufgerufen werden damit der Ball dem TW folgt.
|
||||
*/
|
||||
public void updateHeldPosition() {
|
||||
if (!heldByGoalkeeper || holdingPlayer == null || entity == null || entity.isDead()) return;
|
||||
Location hold = holdingPlayer.getLocation().clone();
|
||||
@@ -198,43 +360,39 @@ public class Ball {
|
||||
hold.setY(hold.getY() + 1.0);
|
||||
entity.teleport(hold);
|
||||
entity.setVelocity(new Vector(0, 0, 0));
|
||||
// Sanfte Rotation während der Ball gehalten wird
|
||||
rollAngle += 0.04;
|
||||
entity.setHeadPose(new EulerAngle(0, rollAngle, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Torwart lässt den Ball los ohne zu werfen (z.B. fallen lassen).
|
||||
*/
|
||||
public void releaseBall() {
|
||||
this.heldByGoalkeeper = false;
|
||||
this.holdingPlayer = null;
|
||||
if (entity != null && !entity.isDead()) {
|
||||
entity.setGravity(true);
|
||||
}
|
||||
if (entity != null && !entity.isDead()) entity.setGravity(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Torwart wirft den Ball mit gegebener Stärke (0.5 – 2.5).
|
||||
*/
|
||||
public void throwBall(Player goalkeeper, double power) {
|
||||
releaseBall();
|
||||
if (entity == null || entity.isDead()) return;
|
||||
Vector dir = goalkeeper.getLocation().getDirection();
|
||||
dir.setY(dir.getY() + 0.25);
|
||||
dir.multiply(power);
|
||||
Vector dir = goalkeeper.getLocation().getDirection().clone();
|
||||
dir.setY(dir.getY() + 0.3);
|
||||
dir.multiply(Math.min(power, MAX_VELOCITY * 0.75));
|
||||
entity.setVelocity(dir);
|
||||
prevYVelocity = dir.getY();
|
||||
Location loc = entity.getLocation();
|
||||
loc.getWorld().playSound(loc, Sound.ENTITY_SNOWBALL_THROW, 1f, 1.1f);
|
||||
loc.getWorld().spawnParticle(Particle.POOF, loc, 5, 0.1, 0.1, 0.1, 0.04);
|
||||
}
|
||||
|
||||
public boolean isHeld() { return heldByGoalkeeper; }
|
||||
public Player getHoldingPlayer() { return holdingPlayer; }
|
||||
|
||||
// ── Util ─────────────────────────────────────────────────────────────────
|
||||
// ── Util ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public void returnToCenter() {
|
||||
if (entity != null && !entity.isDead()) {
|
||||
entity.teleport(spawnLocation);
|
||||
entity.setVelocity(new Vector(0, 0, 0));
|
||||
entity.setHeadPose(new EulerAngle(0, 0, 0));
|
||||
prevYVelocity = 0;
|
||||
rollAngle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,8 +403,10 @@ public class Ball {
|
||||
}
|
||||
|
||||
public ArmorStand getEntity() { return entity; }
|
||||
public boolean isActive() { return active; }
|
||||
public Location getSpawnLocation() { return spawnLocation; }
|
||||
public boolean isActive() { return active; }
|
||||
public Location getSpawnLocation() { return spawnLocation; }
|
||||
public boolean isHeld() { return heldByGoalkeeper; }
|
||||
public Player getHoldingPlayer() { return holdingPlayer; }
|
||||
|
||||
public double getDistanceTo(Player player) {
|
||||
if (entity == null || entity.isDead()) return Double.MAX_VALUE;
|
||||
|
||||
Reference in New Issue
Block a user