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

@@ -42,12 +42,14 @@ public class StatsManager {
String path = "players." + uuidStr;
PlayerStats stats = new PlayerStats(
statsConfig.getString(path + ".name", "Unbekannt"),
statsConfig.getInt(path + ".goals", 0),
statsConfig.getInt(path + ".kicks", 0),
statsConfig.getInt(path + ".wins", 0),
statsConfig.getInt(path + ".losses", 0),
statsConfig.getInt(path + ".draws", 0),
statsConfig.getInt(path + ".games", 0)
statsConfig.getInt(path + ".goals", 0),
statsConfig.getInt(path + ".kicks", 0),
statsConfig.getInt(path + ".wins", 0),
statsConfig.getInt(path + ".losses", 0),
statsConfig.getInt(path + ".draws", 0),
statsConfig.getInt(path + ".games", 0),
statsConfig.getInt(path + ".assists", 0),
statsConfig.getInt(path + ".ownGoals", 0)
);
cache.put(uuid, stats);
} catch (IllegalArgumentException ignored) {}
@@ -61,13 +63,15 @@ public class StatsManager {
for (Map.Entry<UUID, PlayerStats> entry : cache.entrySet()) {
String path = "players." + entry.getKey();
PlayerStats s = entry.getValue();
statsConfig.set(path + ".name", s.name);
statsConfig.set(path + ".goals", s.goals);
statsConfig.set(path + ".kicks", s.kicks);
statsConfig.set(path + ".wins", s.wins);
statsConfig.set(path + ".losses", s.losses);
statsConfig.set(path + ".draws", s.draws);
statsConfig.set(path + ".games", s.games);
statsConfig.set(path + ".name", s.name);
statsConfig.set(path + ".goals", s.goals);
statsConfig.set(path + ".kicks", s.kicks);
statsConfig.set(path + ".wins", s.wins);
statsConfig.set(path + ".losses", s.losses);
statsConfig.set(path + ".draws", s.draws);
statsConfig.set(path + ".games", s.games);
statsConfig.set(path + ".assists", s.assists);
statsConfig.set(path + ".ownGoals", s.ownGoals);
}
try { statsConfig.save(statsFile); } catch (IOException e) { e.printStackTrace(); }
}
@@ -75,7 +79,7 @@ public class StatsManager {
// ── Datenzugriff ────────────────────────────────────────────────────────
public PlayerStats getStats(UUID uuid) {
return cache.computeIfAbsent(uuid, k -> new PlayerStats("Unbekannt", 0, 0, 0, 0, 0, 0));
return cache.computeIfAbsent(uuid, k -> new PlayerStats("Unbekannt", 0, 0, 0, 0, 0, 0, 0, 0));
}
public void addGoal(UUID uuid, String name) {
@@ -85,6 +89,20 @@ public class StatsManager {
save();
}
public void addOwnGoal(UUID uuid, String name) {
PlayerStats s = getStats(uuid);
s.name = name;
s.ownGoals++;
save();
}
public void addAssist(UUID uuid, String name) {
PlayerStats s = getStats(uuid);
s.name = name;
s.assists++;
save();
}
public void addKick(UUID uuid, String name) {
PlayerStats s = getStats(uuid);
s.name = name;
@@ -131,11 +149,12 @@ public class StatsManager {
public static class PlayerStats {
public String name;
public int goals, kicks, wins, losses, draws, games;
public int goals, kicks, wins, losses, draws, games, assists, ownGoals;
public PlayerStats(String name, int goals, int kicks, int wins, int losses, int draws, int games) {
public PlayerStats(String name, int goals, int kicks, int wins, int losses, int draws, int games, int assists, int ownGoals) {
this.name = name; this.goals = goals; this.kicks = kicks;
this.wins = wins; this.losses = losses; this.draws = draws; this.games = games;
this.assists = assists; this.ownGoals = ownGoals;
}
public double getWinRate() {

View File

@@ -13,6 +13,8 @@ public class Arena implements ConfigurationSerializable {
private Location center, redSpawn, blueSpawn, ballSpawn;
private Location redGoalMin, redGoalMax, blueGoalMin, blueGoalMax, lobby;
private Location fieldMin, fieldMax;
// Strafräume optional manuell gesetzt; sonst auto-berechnet aus Tor + config
private Location redPenaltyMin, redPenaltyMax, bluePenaltyMin, bluePenaltyMax;
private int minPlayers, maxPlayers, gameDuration;
/**
@@ -43,6 +45,82 @@ public class Arena implements ConfigurationSerializable {
public boolean isInRedGoal(Location loc) { return isInRegion(loc, redGoalMin, redGoalMax); }
public boolean isInBlueGoal(Location loc) { return isInRegion(loc, blueGoalMin, blueGoalMax); }
// ── Strafraum ────────────────────────────────────────────────────────────
/**
* Prüft ob eine Position im roten Strafraum liegt.
* Wenn manuell gesetzte Punkte vorhanden sind, werden diese verwendet.
* Sonst wird der Strafraum automatisch aus den Tor-Koordinaten + config berechnet.
*/
public boolean isInRedPenaltyArea(Location loc) {
if (redPenaltyMin != null && redPenaltyMax != null) {
return isInRegion2D(loc, redPenaltyMin, redPenaltyMax);
}
return isAutoCalculatedPenaltyArea(loc, redGoalMin, redGoalMax, true);
}
public boolean isInBluePenaltyArea(Location loc) {
if (bluePenaltyMin != null && bluePenaltyMax != null) {
return isInRegion2D(loc, bluePenaltyMin, bluePenaltyMax);
}
return isAutoCalculatedPenaltyArea(loc, blueGoalMin, blueGoalMax, false);
}
/**
* Automatisch berechneter Strafraum.
* Erweitert das Tor um 'penalty-area-margin' seitwärts und 'penalty-area-depth'
* in Richtung Spielfeldmitte.
*/
private boolean isAutoCalculatedPenaltyArea(Location loc, Location gMin, Location gMax, boolean isRedGoal) {
if (gMin == null || gMax == null || loc == null) return false;
Fussball plugin = Fussball.getInstance();
double depth = plugin != null ? plugin.getConfig().getDouble("gameplay.penalty-area-depth", 16) : 16;
double margin = plugin != null ? plugin.getConfig().getDouble("gameplay.penalty-area-margin", 6) : 6;
double minX = Math.min(gMin.getX(), gMax.getX()) - margin;
double maxX = Math.max(gMin.getX(), gMax.getX()) + margin;
double minZ = Math.min(gMin.getZ(), gMax.getZ()) - margin;
double maxZ = Math.max(gMin.getZ(), gMax.getZ()) + margin;
org.bukkit.util.Vector dir = getFieldDirection();
if (dir != null) {
double redAxis = getRedGoalAxisValue();
double blueAxis = getBlueGoalAxisValue();
// Strafraum erstreckt sich VOM Tor in Richtung Feldmitte
if (Math.abs(dir.getZ()) > Math.abs(dir.getX())) {
// Feld läuft entlang Z-Achse
if (isRedGoal) {
if (redAxis < blueAxis) maxZ = Math.max(maxZ, maxZ + depth);
else minZ = Math.min(minZ, minZ - depth);
} else {
if (blueAxis > redAxis) minZ = Math.min(minZ, minZ - depth);
else maxZ = Math.max(maxZ, maxZ + depth);
}
} else {
// Feld läuft entlang X-Achse
if (isRedGoal) {
if (redAxis < blueAxis) maxX = Math.max(maxX, maxX + depth);
else minX = Math.min(minX, minX - depth);
} else {
if (blueAxis > redAxis) minX = Math.min(minX, minX - depth);
else maxX = Math.max(maxX, maxX + depth);
}
}
} else {
// Keine Richtungsinformation → gleichmäßig ausdehnen
minX -= depth / 2; maxX += depth / 2;
minZ -= depth / 2; maxZ += depth / 2;
}
return loc.getX() >= minX && loc.getX() <= maxX && loc.getZ() >= minZ && loc.getZ() <= maxZ;
}
private boolean isInRegion2D(Location loc, Location min, Location max) {
if (min == null || max == null || loc == null) return false;
return loc.getX() >= Math.min(min.getX(), max.getX()) && loc.getX() <= Math.max(min.getX(), max.getX())
&& loc.getZ() >= Math.min(min.getZ(), max.getZ()) && loc.getZ() <= Math.max(min.getZ(), max.getZ());
}
/**
* BUG FIX: Nur X und Z prüfen (Y ignorieren).
* Vorher führte die Y-Prüfung dazu, dass der Ball beim Anstoß sofort
@@ -173,6 +251,10 @@ public class Arena implements ConfigurationSerializable {
if (blueGoalMax != null) map.put("blueGoalMax", serLoc(blueGoalMax));
if (fieldMin != null) map.put("fieldMin", serLoc(fieldMin));
if (fieldMax != null) map.put("fieldMax", serLoc(fieldMax));
if (redPenaltyMin != null) map.put("redPenaltyMin", serLoc(redPenaltyMin));
if (redPenaltyMax != null) map.put("redPenaltyMax", serLoc(redPenaltyMax));
if (bluePenaltyMin != null) map.put("bluePenaltyMin", serLoc(bluePenaltyMin));
if (bluePenaltyMax != null) map.put("bluePenaltyMax", serLoc(bluePenaltyMax));
return map;
}
@@ -197,6 +279,10 @@ public class Arena implements ConfigurationSerializable {
if (map.containsKey("blueGoalMax")) a.blueGoalMax = desLoc(map.get("blueGoalMax"));
if (map.containsKey("fieldMin")) a.fieldMin = desLoc(map.get("fieldMin"));
if (map.containsKey("fieldMax")) a.fieldMax = desLoc(map.get("fieldMax"));
if (map.containsKey("redPenaltyMin")) a.redPenaltyMin = desLoc(map.get("redPenaltyMin"));
if (map.containsKey("redPenaltyMax")) a.redPenaltyMax = desLoc(map.get("redPenaltyMax"));
if (map.containsKey("bluePenaltyMin")) a.bluePenaltyMin = desLoc(map.get("bluePenaltyMin"));
if (map.containsKey("bluePenaltyMax")) a.bluePenaltyMax = desLoc(map.get("bluePenaltyMax"));
return a;
}
@@ -244,6 +330,16 @@ public class Arena implements ConfigurationSerializable {
public void setFieldMin(Location l) { this.fieldMin = l; }
public Location getFieldMax() { return fieldMax; }
public void setFieldMax(Location l) { this.fieldMax = l; }
public Location getRedPenaltyMin() { return redPenaltyMin; }
public void setRedPenaltyMin(Location l) { this.redPenaltyMin = l; }
public Location getRedPenaltyMax() { return redPenaltyMax; }
public void setRedPenaltyMax(Location l) { this.redPenaltyMax = l; }
public Location getBluePenaltyMin() { return bluePenaltyMin; }
public void setBluePenaltyMin(Location l) { this.bluePenaltyMin = l; }
public Location getBluePenaltyMax() { return bluePenaltyMax; }
public void setBluePenaltyMax(Location l) { this.bluePenaltyMax = l; }
public boolean hasManualpPenaltyAreas() { return redPenaltyMin != null && redPenaltyMax != null
&& bluePenaltyMin != null && bluePenaltyMax != null; }
public int getMinPlayers() { return minPlayers; }
public void setMinPlayers(int n) { this.minPlayers = n; }
public int getMaxPlayers() { return maxPlayers; }

View File

@@ -83,6 +83,8 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
StatsManager.PlayerStats s = plugin.getStatsManager().getStats(target.getUniqueId());
player.sendMessage(MessageUtil.header("Statistiken: " + target.getName()));
player.sendMessage("§7 ⚽ Tore: §e" + s.goals);
player.sendMessage("§7 🅾 Eigentore: §c" + s.ownGoals);
player.sendMessage("§7 🎯 Vorlagen: §a" + s.assists);
player.sendMessage("§7 👟 Schüsse: §e" + s.kicks);
player.sendMessage("§7 🏆 Siege: §a" + s.wins);
player.sendMessage("§7 ❌ Niederlagen: §c" + s.losses);
@@ -205,8 +207,12 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
case "redgoalmax" -> { arena.setRedGoalMax(player.getLocation()); player.sendMessage(MessageUtil.success("Rotes Tor Max gesetzt: " + locStr(player.getLocation()))); }
case "bluegoalmin" -> { arena.setBlueGoalMin(player.getLocation()); player.sendMessage(MessageUtil.success("Blaues Tor Min gesetzt: " + locStr(player.getLocation()))); }
case "bluegoalmax" -> { arena.setBlueGoalMax(player.getLocation()); player.sendMessage(MessageUtil.success("Blaues Tor Max gesetzt: " + locStr(player.getLocation()))); }
case "fieldmin" -> { arena.setFieldMin(player.getLocation()); player.sendMessage(MessageUtil.success("Spielfeld Min gesetzt: " + locStr(player.getLocation()))); }
case "fieldmax" -> { arena.setFieldMax(player.getLocation()); player.sendMessage(MessageUtil.success("Spielfeld Max gesetzt: " + locStr(player.getLocation()))); }
case "fieldmin" -> { arena.setFieldMin(player.getLocation()); player.sendMessage(MessageUtil.success("Spielfeld Min gesetzt: " + locStr(player.getLocation()))); }
case "fieldmax" -> { arena.setFieldMax(player.getLocation()); player.sendMessage(MessageUtil.success("Spielfeld Max gesetzt: " + locStr(player.getLocation()))); }
case "redpenaltymin" -> { arena.setRedPenaltyMin(player.getLocation()); player.sendMessage(MessageUtil.success("Roter Strafraum Min gesetzt: " + locStr(player.getLocation()))); }
case "redpenaltymax" -> { arena.setRedPenaltyMax(player.getLocation()); player.sendMessage(MessageUtil.success("Roter Strafraum Max gesetzt: " + locStr(player.getLocation()))); }
case "bluepenaltymin" -> { arena.setBluePenaltyMin(player.getLocation()); player.sendMessage(MessageUtil.success("Blauer Strafraum Min gesetzt: "+ locStr(player.getLocation()))); }
case "bluepenaltymax" -> { arena.setBluePenaltyMax(player.getLocation()); player.sendMessage(MessageUtil.success("Blauer Strafraum Max gesetzt: "+ locStr(player.getLocation()))); }
case "minplayers" -> { if (args.length < 4) return; arena.setMinPlayers(Integer.parseInt(args[3])); player.sendMessage(MessageUtil.success("Min-Spieler: §e" + args[3])); }
case "maxplayers" -> { if (args.length < 4) return; arena.setMaxPlayers(Integer.parseInt(args[3])); player.sendMessage(MessageUtil.success("Max-Spieler: §e" + args[3])); }
case "duration" -> { if (args.length < 4) return; arena.setGameDuration(Integer.parseInt(args[3])); player.sendMessage(MessageUtil.success("Spieldauer: §e" + args[3] + "s")); }
@@ -220,6 +226,10 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
player.sendMessage("§7 Rotes Tor: " + check(arena.getRedGoalMin(), arena.getRedGoalMax()));
player.sendMessage("§7 Blaues Tor: " + check(arena.getBlueGoalMin(), arena.getBlueGoalMax()));
player.sendMessage("§7 Spielfeld: " + check(arena.getFieldMin(), arena.getFieldMax()) + " §8(optional)");
player.sendMessage("§7 Roter Strafraum: " + check(arena.getRedPenaltyMin(), arena.getRedPenaltyMax())
+ " §8(optional sonst auto-berechnet)");
player.sendMessage("§7 Blauer Strafraum: " + check(arena.getBluePenaltyMin(), arena.getBluePenaltyMax())
+ " §8(optional sonst auto-berechnet)");
player.sendMessage("§7 Min. Spieler: §e" + arena.getMinPlayers());
player.sendMessage("§7 Max. Spieler: §e" + arena.getMaxPlayers());
player.sendMessage("§7 Spieldauer: §e" + arena.getGameDuration() + "s");
@@ -300,6 +310,7 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
p.sendMessage("§e/fb setup <arena> lobby|redspawn|bluespawn|ballspawn|center");
p.sendMessage("§e/fb setup <arena> redgoalmin|redgoalmax|bluegoalmin|bluegoalmax");
p.sendMessage("§e/fb setup <arena> fieldmin|fieldmax §8(optional Aus-Erkennung)");
p.sendMessage("§e/fb setup <arena> redpenaltymin|redpenaltymax|bluepenaltymin|bluepenaltymax §8(optional auto-berechnet wenn leer)");
p.sendMessage("§e/fb setup <arena> minplayers <n>|maxplayers <n>|duration <s>|info");
}
@@ -322,7 +333,9 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
} else if (args.length == 3 && args[0].equalsIgnoreCase("setup")) {
list.addAll(List.of("lobby","redspawn","bluespawn","ballspawn","center",
"redgoalmin","redgoalmax","bluegoalmin","bluegoalmax",
"fieldmin","fieldmax","minplayers","maxplayers","duration","info"));
"fieldmin","fieldmax",
"redpenaltymin","redpenaltymax","bluepenaltymin","bluepenaltymax",
"minplayers","maxplayers","duration","info"));
} else if (args.length == 2 && args[0].equalsIgnoreCase("top")) {
list.addAll(List.of("goals", "wins"));
}

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;

View File

@@ -53,8 +53,17 @@ public class Game {
// ────────────────────────────────────────────────────────────────────────
private UUID lastKicker = null;
private UUID secondLastKicker = null; // für Assist-Erkennung
private boolean lastKickWasHeader = false; // für Rückpass-Regel (Header erlaubt)
private Team lastTouchTeam = null;
private Team throwInTeam = null; // null = alle dürfen; gesetzt = nur dieses Team darf treten
private Team throwInTeam = null;
// ── Nachspielzeit ──────────────────────────────────────────────────────
private int injuryTimeBuffer = 0; // Angesammelte Nachspielzeit in Sekunden
private boolean inInjuryTime = false; // Läuft gerade Nachspielzeit?
// ── Kopfball-Abklingzeit ───────────────────────────────────────────────
private final Map<UUID, Integer> headerCooldowns = new HashMap<>();
// ── Ball ───────────────────────────────────────────────────────────────
private Ball ball;
@@ -562,15 +571,43 @@ public class Game {
private void startGameLoop() {
gameTask = new BukkitRunnable() {
public void run() {
// Nur bei RUNNING oder OVERTIME zählen
if (state != GameState.RUNNING && state != GameState.OVERTIME) return;
// Kopfball-Abklingzeiten herunterzählen
headerCooldowns.entrySet().removeIf(e -> {
int t = e.getValue() - 1;
e.setValue(t);
return t <= 0;
});
// Halbzeit-Check VOR dem timeLeft-Decrement (1. Halbzeit)
if (!secondHalf && state == GameState.RUNNING && timeLeft == arena.getGameDuration() / 2) {
// Nachspielzeit abbauen bevor Halbzeit
if (injuryTimeBuffer > 0) {
injuryTimeBuffer--;
int itLeft = injuryTimeBuffer;
if (!inInjuryTime) {
inInjuryTime = true;
int mins = (int) Math.ceil((itLeft + 1) / 60.0);
broadcastAll(Messages.get("injury-time", "n", String.valueOf(mins)));
for (UUID uuid : allPlayers) {
Player p = Bukkit.getPlayer(uuid);
if (p != null) p.sendTitle("§c⏱", "§8+" + (itLeft + 1) + "s Nachspielzeit", 5, 30, 5);
}
}
scoreboard.updateAll(); updateBossBar(); return;
}
inInjuryTime = false;
startHalfTime(); return;
}
timeLeft--;
secondsPlayed++;
scoreboard.updateAll();
updateBossBar();
checkPlayerBallInteraction();
checkPlayerBoundaries();
checkHeaderOpportunities();
// Freistoß-Abstandsdurchsetzung
if (freekickLocation != null) {
@@ -579,7 +616,7 @@ public class Game {
if (freekickTicks <= 0) { freekickLocation = null; throwInTeam = null; }
}
// Warn-Nachrichten aus config
// Warn-Nachrichten
if (timeLeft == 60) broadcastAll(Messages.get("time-1min"));
if (timeLeft == 30) broadcastAll(Messages.get("time-30sec"));
if (timeLeft <= 10 && timeLeft > 0) {
@@ -587,22 +624,31 @@ public class Game {
for (UUID uuid : allPlayers) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 2f); }
}
// Halbzeit-Check (nur 1. Halbzeit)
if (!secondHalf && state == GameState.RUNNING && timeLeft == arena.getGameDuration() / 2) {
startHalfTime(); return;
}
// Zeit abgelaufen
// Zeit abgelaufen → Nachspielzeit prüfen
if (timeLeft <= 0) {
if (injuryTimeBuffer > 0) {
injuryTimeBuffer--;
if (!inInjuryTime) {
inInjuryTime = true;
int mins = (int) Math.ceil((injuryTimeBuffer + 1) / 60.0);
broadcastAll(Messages.get("injury-time", "n", String.valueOf(mins)));
for (UUID uuid : allPlayers) {
Player p = Bukkit.getPlayer(uuid);
if (p != null) p.sendTitle("§c+"+mins+"'", "§8Nachspielzeit!", 5, 30, 5);
}
}
scoreboard.updateAll(); updateBossBar(); return;
}
inInjuryTime = false;
if (state == GameState.OVERTIME) {
Team winner = getWinnerTeam();
if (winner != null) { endGame(winner); }
else { startPenalty(); }
if (winner != null) { endGame(winner); } else { startPenalty(); }
} else {
Team winner = getWinnerTeam();
if (winner != null) { endGame(winner); }
if (winner != null) { endGame(winner); }
else if (!overtimeDone) { startOvertime(); }
else { startPenalty(); }
else { startPenalty(); }
}
cancel();
}
@@ -646,7 +692,7 @@ public class Game {
return;
}
ball.applyFriction();
ball.applyPhysics();
Location current = ball.getEntity().getLocation();
if (lastBallLocation == null || !lastBallLocation.getWorld().equals(current.getWorld())) {
@@ -769,6 +815,7 @@ public class Game {
broadcastAll(message);
for (UUID uuid : allPlayers) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 1f, 1.2f); }
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-out", 3));
final Location spawnHere = resumeLocation;
new BukkitRunnable() {
@@ -817,35 +864,80 @@ public class Game {
if (ball != null) { ball.remove(); ball = null; }
lastBallLocation = null;
// Torschütze ermitteln
// ── Eigentor-Erkennung ──────────────────────────────────────────────
boolean ownGoal = false;
if (lastKicker != null) {
Player possibleScorer = Bukkit.getPlayer(lastKicker);
if (possibleScorer != null) {
Team scorerTeam = getTeam(possibleScorer);
// Scorerteam ist das GEGNER-Team des schießenden Spielers → Eigentor
if (scorerTeam != null && scorerTeam == scoringTeam.getOpponent()) {
ownGoal = true;
}
}
}
// ── Torschütze / Eigentor ────────────────────────────────────────────
String scorerName = "Unbekannt";
String assistName = null;
if (lastKicker != null) {
Player scorer = Bukkit.getPlayer(lastKicker);
if (scorer != null) {
scorerName = scorer.getName();
goals.merge(lastKicker, 1, Integer::sum);
// Persistente Statistik
plugin.getStatsManager().addGoal(lastKicker, scorer.getName());
if (ownGoal) {
goals.merge(lastKicker, 1, Integer::sum); // Eigentor separat in Match-Log
plugin.getStatsManager().addOwnGoal(lastKicker, scorer.getName());
} else {
goals.merge(lastKicker, 1, Integer::sum);
plugin.getStatsManager().addGoal(lastKicker, scorer.getName());
// ── Assist-Erkennung ────────────────────────────────────
if (secondLastKicker != null && !secondLastKicker.equals(lastKicker)) {
Player assistPlayer = Bukkit.getPlayer(secondLastKicker);
if (assistPlayer != null && getTeam(assistPlayer) == scoringTeam) {
assistName = assistPlayer.getName();
plugin.getStatsManager().addAssist(secondLastKicker, assistPlayer.getName());
}
}
}
}
lastKicker = null;
}
lastTouchTeam = null; // Reset nach Tor
secondLastKicker = null;
lastTouchTeam = null;
// ── Nachspielzeit hinzufügen ─────────────────────────────────────────
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-goal", 30));
// ── Broadcast ────────────────────────────────────────────────────────
String color = scoringTeam == Team.RED ? "§c" : "§9";
broadcastAll("§e§l╔══════════════════╗");
broadcastAll("§e§l║ ⚽ T O R !! ║");
if (ownGoal) {
broadcastAll("§c§l║ ⚽ EIGENTOR!! ║");
} else {
broadcastAll("§e§l║ ⚽ T O R !! ║");
}
broadcastAll("§e§l╚══════════════════╝");
broadcastAll(color + "" + scoringTeam.getDisplayName() + " §7hat ein Tor erzielt!");
broadcastAll("§7Torschütze: §e" + scorerName);
if (ownGoal) {
broadcastAll(Messages.get("own-goal", "player", scorerName));
} else {
broadcastAll("§7Torschütze: §e" + scorerName);
if (assistName != null) broadcastAll(Messages.get("assist", "player", assistName));
}
broadcastAll("§7Spielstand: §c" + redScore + " §7: §9" + blueScore);
// Tor-Replay in Action-Bar (auch für Zuschauer)
String replayMsg = "§eTOR von §f" + scorerName + "§e! §8[" + color + scoringTeam.getDisplayName() + "§8] §c" + redScore + " §7: §9" + blueScore;
String replayMsg = ownGoal
? "§cEIGENTOR §f" + scorerName + "§e! §c" + redScore + " §7: §9" + blueScore
: "§e⚽ TOR von §f" + scorerName + "§e! §8[" + color + scoringTeam.getDisplayName() + "§8] §c" + redScore + " §7: §9" + blueScore;
for (UUID uuid : getAllAndSpectators()) {
Player p = Bukkit.getPlayer(uuid);
if (p != null) {
p.sendTitle(color + "⚽ TOR!", "§7" + scoringTeam.getDisplayName() + " §8| §c" + redScore + " §7: §9" + blueScore, 10, 60, 10);
p.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(replayMsg));
String titleColor = ownGoal ? "§c" : color;
p.sendTitle(titleColor + (ownGoal ? "⚽ EIGENTOR!" : "⚽ TOR!"),
"§7" + scoringTeam.getDisplayName() + " §8| §c" + redScore + " §7: §9" + blueScore, 10, 60, 10);
p.spigot().sendMessage(net.md_5.bungee.api.ChatMessageType.ACTION_BAR,
net.md_5.bungee.api.chat.TextComponent.fromLegacyText(replayMsg));
spawnFirework(arena.getBallSpawn(), scoringTeam);
p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 2f, 1f);
}
@@ -853,15 +945,24 @@ public class Game {
scoreboard.updateAll();
updateBossBar();
refreshSigns();
logMatchEvent("§eTOR: §f" + scorerName + " §8[" + color + scoringTeam.getDisplayName() + "§8] §7" + redScore + ":" + blueScore);
String ownGoalMarker = ownGoal ? " §8(Eigentor)" : "";
logMatchEvent("§eTOR: §f" + scorerName + ownGoalMarker
+ (assistName != null ? " §8(Vorlage: §e" + assistName + "§8)" : "")
+ " §8[" + color + scoringTeam.getDisplayName() + "§8] §7" + redScore + ":" + blueScore);
final Team concedingTeam = scoringTeam.getOpponent(); // für Anstoß
new BukkitRunnable() {
public void run() {
if (state == GameState.GOAL) {
state = GameState.RUNNING;
outOfBoundsCountdown.clear();
outOfBoundsCountdown.clear();
spawnBallDelayed(arena.getBallSpawn());
// BUG FIX: In 2. Halbzeit Seiten getauscht
// ── Anstoß-Team: das Team, das den Treffer kassiert hat ──
throwInTeam = concedingTeam;
broadcastAll(Messages.get("kickoff-team", "team",
concedingTeam == Team.RED ? "§cRotes Team" : "§9Blaues Team"));
// Teleportation je nach Halbzeit
if (!secondHalf) {
for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getRedSpawn()); }
for (UUID uuid : blueTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getBlueSpawn()); }
@@ -892,13 +993,19 @@ public class Game {
private void updateBossBar() {
if (bossBar == null) return;
int m = timeLeft / 60, s = timeLeft % 60;
String timeStr = String.format("%02d:%02d", m, s);
String timeStr;
if (inInjuryTime) {
int injMins = (int) Math.ceil(injuryTimeBuffer / 60.0);
timeStr = "§c+" + injMins + "' ";
} else {
int m = timeLeft / 60, s = timeLeft % 60;
timeStr = String.format("%02d:%02d", m, s);
}
String halfLabel = state == GameState.OVERTIME ? "§6VL" : (secondHalf ? "§72.HZ" : "§71.HZ");
bossBar.setTitle("§c" + redScore + " §7: §9" + blueScore + " §8│ §e⏱ " + timeStr + " " + halfLabel);
double progress = Math.max(0.0, Math.min(1.0, (double) timeLeft / (state == GameState.OVERTIME ? 600 : arena.getGameDuration())));
bossBar.setProgress(progress);
bossBar.setColor(timeLeft > 60 ? BarColor.GREEN : timeLeft > 20 ? BarColor.YELLOW : BarColor.RED);
bossBar.setProgress(inInjuryTime ? Math.max(0.01, progress) : progress);
bossBar.setColor(inInjuryTime ? BarColor.RED : (timeLeft > 60 ? BarColor.GREEN : timeLeft > 20 ? BarColor.YELLOW : BarColor.RED));
}
// ════════════════════════════════════════════════════════════════════════
@@ -1022,8 +1129,39 @@ public class Game {
fouler.playSound(fouler.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
if (directRed) giveRedCard(fouler, "Grobes Foulspiel");
else giveYellowCard(fouler, "Foulspiel");
startFreekick(victimTeam, foulLocation, Messages.get("foul", "player", fouler.getName()));
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-foul", 5));
logMatchEvent("§cFoul: §e" + fouler.getName() + " §7→ §e" + victim.getName());
// ── Foul im Strafraum → Elfmeter ───────────────────────────────────
boolean inRedPenalty = arena.isInRedPenaltyArea(foulLocation);
boolean inBluePenalty = arena.isInBluePenaltyArea(foulLocation);
boolean penaltyKick = false;
if (inRedPenalty && victimTeam == Team.BLUE) {
// Foul an Blau im roten Strafraum → Elfmeter für Blau
broadcastAll(Messages.get("foul-penalty", "team", "§9Blaues Team"));
for (UUID uuid : getAllAndSpectators()) {
Player p = Bukkit.getPlayer(uuid);
if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§9Blaues Team§7 schießt!", 5, 50, 10);
}
penaltyKick = true;
// Elfmeter als Freistoß direkt auf Ballspawn (ggf. später: separater Elfmeter-Punkt)
startFreekick(Team.BLUE, arena.getBallSpawn(), "Elfmeter");
} else if (inBluePenalty && victimTeam == Team.RED) {
// Foul an Rot im blauen Strafraum → Elfmeter für Rot
broadcastAll(Messages.get("foul-penalty", "team", "§cRotes Team"));
for (UUID uuid : getAllAndSpectators()) {
Player p = Bukkit.getPlayer(uuid);
if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§cRotes Team§7 schießt!", 5, 50, 10);
}
penaltyKick = true;
startFreekick(Team.RED, arena.getBallSpawn(), "Elfmeter");
}
if (!penaltyKick) {
startFreekick(victimTeam, foulLocation, Messages.get("foul", "player", fouler.getName()));
}
}
private void giveYellowCard(Player player, String reason) {
@@ -1033,6 +1171,7 @@ public class Game {
broadcastAll(Messages.get("yellow-card-2", "player", player.getName()));
player.sendTitle("§e🟨→§c🟥", "§7Gelb-Rot Karte!", 5, 60, 10);
logMatchEvent("§e🟨§c🟥 §7Gelb-Rot: §e" + player.getName());
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-card", 15));
new BukkitRunnable() { public void run() { disqualifyPlayer(player); } }.runTaskLater(plugin, 60L);
} else {
broadcastAll(Messages.get("yellow-card", "player", player.getName(), "reason", reason));
@@ -1049,6 +1188,7 @@ public class Game {
player.sendTitle(Messages.get("red-card-title"), Messages.get("red-card-sub"), 5, 80, 10);
player.playSound(player.getLocation(), Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 1f, 1f);
logMatchEvent("§c🟥 §7Rot: §e" + player.getName() + " §8(" + reason + ")");
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-card", 15));
new BukkitRunnable() { public void run() { disqualifyPlayer(player); } }.runTaskLater(plugin, 80L);
}
@@ -1123,6 +1263,62 @@ public class Game {
}
}
// ════════════════════════════════════════════════════════════════════════
// KOPFBALL
// ════════════════════════════════════════════════════════════════════════
/**
* Prüft jeden Sekunden-Tick ob ein Spieler den Ball köpfen kann.
* Bedingungen: Spieler ist in der Luft, Ball befindet sich auf Kopfhöhe,
* kein aktiver Kopfball-Cooldown.
*/
private void checkHeaderOpportunities() {
if (ball == null || !ball.isActive() || ball.isHeld()) return;
if (state != GameState.RUNNING && state != GameState.OVERTIME) return;
Location ballLoc = ball.getEntity() != null ? ball.getEntity().getLocation() : null;
if (ballLoc == null) return;
double headerRange = plugin.getConfig().getDouble("gameplay.header-range", 1.8);
double minHeight = plugin.getConfig().getDouble("gameplay.header-min-height", 0.8);
double maxHeight = plugin.getConfig().getDouble("gameplay.header-max-height", 2.3);
int cooldownTicks = plugin.getConfig().getInt("gameplay.header-cooldown", 10);
for (UUID uuid : allPlayers) {
if (headerCooldowns.containsKey(uuid)) continue; // Abklingzeit läuft noch
Player p = Bukkit.getPlayer(uuid); if (p == null) continue;
// Spieler muss in der Luft sein (nicht auf dem Boden)
if (p.isOnGround()) continue;
Location pLoc = p.getLocation();
if (!pLoc.getWorld().equals(ballLoc.getWorld())) continue;
double dist = pLoc.distance(ballLoc);
if (dist > headerRange) continue;
// Ball muss sich auf Kopfhöhe befinden
double relHeight = ballLoc.getY() - pLoc.getY();
if (relHeight < minHeight || relHeight > maxHeight) continue;
// Einwurf-Prüfung
if (throwInTeam != null && getTeam(p) != throwInTeam) continue;
clearThrowIn();
setLastKickerHeader(uuid);
ball.header(p);
headerCooldowns.put(uuid, cooldownTicks);
// Kurze Broadcast-Meldung
String msg = Messages.get("header", "player", p.getName());
for (UUID u : getAllAndSpectators()) {
Player pl = Bukkit.getPlayer(u);
if (pl != null) pl.spigot().sendMessage(net.md_5.bungee.api.ChatMessageType.ACTION_BAR,
net.md_5.bungee.api.chat.TextComponent.fromLegacyText(msg));
}
break; // pro Tick max. 1 Kopfball
}
}
private double getDistanceOutsideField(Location loc) {
if (arena.getFieldMin() == null || arena.getFieldMax() == null) return 0;
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
@@ -1385,12 +1581,17 @@ public class Game {
public boolean isInGame(Player player) { return allPlayers.contains(player.getUniqueId()); }
public void setLastKicker(UUID uuid) {
// Zweite Berührung tracken (für Assist-Erkennung)
if (lastKicker != null && !lastKicker.equals(uuid)) {
secondLastKicker = lastKicker;
}
this.lastKicker = uuid;
lastKickWasHeader = false; // normaler Schuss / Berührung
Player p = Bukkit.getPlayer(uuid);
if (p != null) lastTouchTeam = getTeam(p);
if (p != null) kicks.merge(uuid, 1, Integer::sum);
// Abseits-Check (nur wenn aktiviert und Spiel läuft)
// Abseits-Check
if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true)
&& (state == GameState.RUNNING || state == GameState.OVERTIME)
&& ball != null && ball.getEntity() != null && !offsideCooldown) {
@@ -1398,6 +1599,31 @@ public class Game {
}
}
/** Wie setLastKicker, aber als Kopfball setzt header-Flag (Rückpass erlaubt) */
public void setLastKickerHeader(UUID uuid) {
if (lastKicker != null && !lastKicker.equals(uuid)) {
secondLastKicker = lastKicker;
}
this.lastKicker = uuid;
lastKickWasHeader = true;
Player p = Bukkit.getPlayer(uuid);
if (p != null) lastTouchTeam = getTeam(p);
if (p != null) kicks.merge(uuid, 1, Integer::sum);
if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true)
&& (state == GameState.RUNNING || state == GameState.OVERTIME)
&& ball != null && ball.getEntity() != null && !offsideCooldown) {
checkOffside(uuid, ball.getEntity().getLocation());
}
}
/** Fügt Nachspielzeit hinzu (aufgerufen bei Toren, Karten, Fouls, Aus-Situationen) */
public void addInjuryTime(int seconds) {
if (!plugin.getConfig().getBoolean("gameplay.injury-time-enabled", true)) return;
if (state != GameState.RUNNING && state != GameState.OVERTIME) return;
injuryTimeBuffer += seconds;
}
public Arena getArena() { return arena; }
public GameState getState() { return state; }
public List<UUID> getRedTeam() { return redTeam; }
@@ -1414,6 +1640,10 @@ public class Game {
public int getPenaltyBlueGoals() { return penaltyBlueGoals; }
/** Gibt zurück welches Team gerade Einwurf/Ecke/Abstoß hat (null = jeder darf) */
public Team getThrowInTeam() { return throwInTeam; }
/** UUID des zuletzt schießenden Spielers */
public UUID getLastKicker() { return lastKicker; }
/** War die letzte Ballaktion ein Kopfball (Rückpass via Kopf ist erlaubt) */
public boolean isLastKickWasHeader() { return lastKickWasHeader; }
/** Berechtigung aufheben wird von BallListener nach dem ersten Schuss gerufen */
public void clearThrowIn() {
throwInTeam = null;

View File

@@ -118,14 +118,20 @@ public class GameManager {
org.bukkit.scheduler.BukkitRunnable task = new org.bukkit.scheduler.BukkitRunnable() {
public void run() {
Queue<UUID> q = queues.get(key);
if (q == null || q.isEmpty()) return;
UUID next = q.poll();
if (next == null) return;
Player p = Bukkit.getPlayer(next);
if (p == null || !p.isOnline()) { run(); return; } // überspringe Offline-Spieler
if (isInAnyGame(p)) return;
p.sendMessage(MessageUtil.success("§e⚽ Dein Platz in §e" + arenaName + " §aist frei! Du wirst hinzugefügt..."));
createGame(arena).addPlayer(p);
if (q == null) return;
// BUG FIX: vorher war hier ein rekursiver run()-Aufruf für Offline-Spieler,
// was bei vielen Offline-Einträgen zu einem StackOverflow führte.
// Jetzt iterativ: alle Offline-Spieler überspringen.
while (!q.isEmpty()) {
UUID next = q.poll();
if (next == null) return;
Player p = Bukkit.getPlayer(next);
if (p == null || !p.isOnline()) continue; // Offline überspringen
if (isInAnyGame(p)) return;
p.sendMessage(MessageUtil.success("§e⚽ Dein Platz in §e" + arenaName + " §aist frei! Du wirst hinzugefügt..."));
createGame(arena).addPlayer(p);
return;
}
}
};
task.runTaskLater(plugin, 120L); // 6s nach Spielende

View File

@@ -7,6 +7,7 @@ import de.fussball.plugin.game.GameState;
import de.fussball.plugin.game.Team;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.entity.*;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@@ -19,6 +20,7 @@ import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.UUID;
/**
* Verwaltet Ball-Interaktionen:
@@ -62,21 +64,40 @@ public class BallListener implements Listener {
// ── Torwart greift Ball (noch nicht haltend) ────────────────────────
if (game.isGoalkeeper(player) && !player.isSneaking()) {
if (!plugin.getConfig().getBoolean("gameplay.foul-detection-enabled", true)) {
// Falls Torwart-Mechanik in Config deaktiviert → normaler Schuss
// Torwart-Mechanik deaktiviert → normaler Schuss
} else if (ball.getDistanceTo(player) <= plugin.getConfig().getDouble("gameplay.goalkeeper-hold-range", 2.5)) {
if (game.isInOwnHalf(player)) {
if (game.getThrowInTeam() == null || game.getThrowInTeam() == game.getTeam(player)) {
// ── RÜCKPASS-REGEL ──────────────────────────────────────────────────
// Torwart darf Ball NICHT mit Händen nehmen wenn ihn ein Mitspieler
// direkt mit dem Fuß zugespielt hat. Kopfball-Rückpässe sind erlaubt.
UUID prevKicker = game.getLastKicker();
if (prevKicker != null && !prevKicker.equals(player.getUniqueId())) {
Player prev = Bukkit.getPlayer(prevKicker);
if (prev != null
&& game.getTeam(prev) == game.getTeam(player)
&& !game.isLastKickWasHeader()) {
player.sendTitle(plugin.getConfig().getString("messages.backpass-title", "§c⚠ RÜCKPASS!"),
"§7Direktes Anspielen mit dem Fuß verboten!", 5, 40, 10);
player.sendMessage(plugin.getConfig().getString("messages.backpass",
"§c⚠ §lRÜCKPASS! §7Torwart darf den Ball nicht mit Händen nehmen!"));
return; // Kein Halten, kein Schuss — Ball rollt weiter
}
}
// ────────────────────────────────────────────────────────────────────
game.clearThrowIn();
game.setLastKicker(player.getUniqueId());
ball.holdBall(player);
game.broadcastAll(plugin.getConfig().getString("messages.goalkeeper-hold",
"§6TW §f{player} §7hält den Ball!").replace("{player}", player.getName()));
return;
if (ball.holdBall(player)) {
game.broadcastAll(plugin.getConfig().getString("messages.goalkeeper-hold",
"§6TW §f{player} §7hält den Ball!").replace("{player}", player.getName()));
return;
}
// holdBall fehlgeschlagen (zu weit vom Tor) → normaler Schuss
}
} else {
player.sendMessage(plugin.getConfig().getString("messages.goalkeeper-no-hold",
"§cDu kannst den Ball nur in deiner eigenen Hälfte halten!"));
// Fällt durch zum normalen Schuss
// TW in gegnerischer Hälfte → normaler Schuss ohne Meldung
}
}
}