Update from Git Manager GUI

This commit is contained in:
2026-02-27 14:17:36 +01:00
parent b2431f5378
commit 65c0568b91
8 changed files with 730 additions and 145 deletions

View File

@@ -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;