Update from Git Manager GUI
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = "§e⚽ TOR von §f" + scorerName + "§e! §8[" + color + scoringTeam.getDisplayName() + "§8] §c" + redScore + " §7: §9" + blueScore;
|
||||
String replayMsg = ownGoal
|
||||
? "§c⚽ EIGENTOR §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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,24 @@ gameplay:
|
||||
out-of-bounds-tolerance: 2.0 # Toleranz außerhalb Spielfeld (Blöcke)
|
||||
out-of-bounds-countdown: 5 # Sekunden bis Disqualifikation
|
||||
|
||||
# ── Strafraum ────────────────────────────────────────────────────────────
|
||||
penalty-area-depth: 16 # Tiefe des automatisch berechneten Strafraums (Blöcke Richtung Feld)
|
||||
penalty-area-margin: 6 # Seitliche Ausdehnung des Strafraums jenseits der Torpfosten
|
||||
|
||||
# ── Kopfball ─────────────────────────────────────────────────────────────
|
||||
header-range: 1.8 # Max. Abstand Spieler→Ball für Kopfball (Blöcke)
|
||||
header-min-height: 0.8 # Min. Höhe des Balls über Spielerfüßen für Kopfball
|
||||
header-max-height: 2.3 # Max. Höhe des Balls über Spielerfüßen für Kopfball
|
||||
header-power: 1.3 # Schussstärke eines Kopfballs
|
||||
header-cooldown: 10 # Ticks Abklingzeit zwischen zwei Kopfbällen desselben Spielers
|
||||
|
||||
# ── Nachspielzeit ────────────────────────────────────────────────────────
|
||||
injury-time-enabled: true
|
||||
injury-time-per-goal: 30 # Sekunden Nachspielzeit pro Tor
|
||||
injury-time-per-card: 15 # Sekunden pro Karte (Gelb-Rot / Rot)
|
||||
injury-time-per-foul: 5 # Sekunden pro Foul
|
||||
injury-time-per-out: 3 # Sekunden pro Aus-Situation
|
||||
|
||||
# ── Nachrichten (alle editierbar) ──────────────────
|
||||
# Verfügbare Platzhalter je nach Kontext:
|
||||
# {player} = Spielername
|
||||
@@ -102,6 +120,28 @@ messages:
|
||||
boundary-disq: "§c⚠ §e{player} §cwurde disqualifiziert! (Spielfeldgrenze)"
|
||||
boundary-disq-self: "§cDu wurdest disqualifiziert, weil du zu lange außerhalb warst!"
|
||||
|
||||
# Eigentore & Assists
|
||||
own-goal: "§c⚽ EIGENTOR! §7{player} hat ins eigene Tor getroffen!"
|
||||
own-goal-title: "§c⚽ EIGENTOR!"
|
||||
assist: "§7Vorlage: §e{player}"
|
||||
|
||||
# Nachspielzeit
|
||||
injury-time: "§c⏱ §l+{n} Min. Nachspielzeit!"
|
||||
injury-time-bar: "§c+{n}' §8│ "
|
||||
|
||||
# Anstoß
|
||||
kickoff-team: "§e⚽ §7Anstoß für {team}§7!"
|
||||
|
||||
# Strafraum / Elfmeter bei Foul
|
||||
foul-penalty: "§c⚠ §lFOUL IM STRAFRAUM! §7Elfmeter für {team}§7!"
|
||||
|
||||
# Rückpass
|
||||
backpass: "§c⚠ §lRÜCKPASS! §7Torwart darf den Ball nicht mit Händen nehmen!"
|
||||
backpass-title: "§c⚠ RÜCKPASS!"
|
||||
|
||||
# Kopfball
|
||||
header: "§e⚽ §7Kopfball von §e{player}§7!"
|
||||
|
||||
# Spieler beitreten / verlassen
|
||||
player-join: "§e{player} §7ist beigetreten! §8({n}/{max})"
|
||||
player-leave: "§e{player} §7hat das Spiel verlassen!"
|
||||
|
||||
Reference in New Issue
Block a user