416 lines
18 KiB
Java
416 lines
18 KiB
Java
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());
|
||
}
|
||
} |