Files
Fussball/src/main/java/de/fussball/plugin/game/Ball.java
2026-02-27 17:45:25 +01:00

416 lines
18 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package de.fussball.plugin.game;
import de.fussball.plugin.Fussball;
import org.bukkit.*;
import org.bukkit.entity.*;
import org.bukkit.event.entity.CreatureSpawnEvent;
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;
import java.net.URL;
import java.util.UUID;
public class Ball {
private static final String BALL_TEXTURE_URL =
"https://textures.minecraft.net/texture/451f8cfcfb85d77945dc6a3618414093e70436b46d2577b28c727f1329b7265e";
// ── 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;
private ArmorStand entity;
private final Location spawnLocation;
private boolean active = false;
// ── 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.spawnLocation = spawnLocation.clone();
}
// ── Config-Helfer ─────────────────────────────────────────────────────────
private double cfg(String path, double def) {
return plugin.getConfig().getDouble(path, def);
}
// ── 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(
spawnLocation, ArmorStand.class, CreatureSpawnEvent.SpawnReason.CUSTOM, false,
stand -> {
stand.setVisible(false);
stand.setGravity(true);
stand.setCollidable(false);
stand.setInvulnerable(true);
stand.setPersistent(false);
stand.setSilent(true);
stand.setSmall(true);
stand.setArms(false);
stand.setBasePlate(false);
stand.setCustomName("§e⚽ Fußball");
stand.setCustomNameVisible(true);
stand.getEquipment().setHelmet(createBallItem());
}
);
if (entity == null) {
plugin.getLogger().severe("[Fussball] ArmorStand konnte nicht gespawnt werden!"); return;
}
active = true;
prevYVelocity = 0.0;
rollAngle = 0.0;
}
private ItemStack createBallItem() {
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta == null) return skull;
meta.setDisplayName("§e⚽ Fußball");
try {
PlayerProfile profile = Bukkit.createPlayerProfile(
UUID.nameUUIDFromBytes("FussballBall".getBytes()), "FussballBall");
PlayerTextures textures = profile.getTextures();
textures.setSkin(new URL(BALL_TEXTURE_URL));
profile.setTextures(textures);
meta.setOwnerProfile(profile);
} catch (MalformedURLException e) {
plugin.getLogger().warning("[Fussball] Ball-Textur URL ungültig: " + e.getMessage());
}
skull.setItemMeta(meta);
return skull;
}
// ── Schüsse ───────────────────────────────────────────────────────────────
/**
* 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("gameplay.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 = 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);
dir.setY(kickVertical);
dir.multiply(player.isSprinting() ? sprintKickPower : kickPower);
applyKick(dir, ballLoc, 1.8f);
}
/**
* 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();
// 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 mult = minPower + (maxPower - minPower) * power;
dir.setY(0.22 + power * 0.28);
dir.multiply(mult);
float pitch = 0.9f + (float)(power * 0.9f);
applyKick(dir, ballLoc, pitch);
if (power > 0.7) {
ballLoc.getWorld().spawnParticle(Particle.FLAME, ballLoc, 14, 0.2, 0.2, 0.2, 0.07);
}
}
/**
* 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 (jeden Tick) ───────────────────────────────────────────────────
/**
* Haupt-Physik-Update jeden Tick vom GoalCheckLoop aufgerufen (ersetzt applyFriction).
* Verarbeitet: Reibung (Luft/Boden), Abprall, Wandreflektion, visuelle Rotation.
*/
public void applyPhysics() {
if (heldByGoalkeeper) return;
if (entity == null || entity.isDead() || !active) 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;
}
public void updateHeldPosition() {
if (!heldByGoalkeeper || holdingPlayer == null || entity == null || entity.isDead()) return;
Location hold = holdingPlayer.getLocation().clone();
hold.add(holdingPlayer.getLocation().getDirection().normalize().multiply(0.8));
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));
}
public void releaseBall() {
this.heldByGoalkeeper = false;
this.holdingPlayer = null;
if (entity != null && !entity.isDead()) entity.setGravity(true);
}
public void throwBall(Player goalkeeper, double power) {
releaseBall();
if (entity == null || entity.isDead()) return;
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);
}
// ── 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;
}
}
public void remove() {
if (entity != null && !entity.isDead()) entity.remove();
entity = null;
active = false;
}
public ArmorStand getEntity() { return entity; }
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;
if (!entity.getWorld().equals(player.getWorld())) return Double.MAX_VALUE;
return entity.getLocation().distance(player.getLocation());
}
}