4 Commits
1.0.3 ... main

Author SHA1 Message Date
a0085ba5bd Update from Git Manager GUI 2026-03-27 15:18:24 +01:00
e10f1313bd Upload via Git Manager GUI - pom.xml 2026-03-27 14:18:23 +00:00
960290dfa8 Update from Git Manager GUI 2026-03-27 10:05:43 +01:00
3f6d91cdc7 Upload via Git Manager GUI - pom.xml 2026-03-27 09:05:42 +00:00
3 changed files with 287 additions and 46 deletions

View File

@@ -79,6 +79,9 @@ public class Game {
private UUID lastKicker = null; private UUID lastKicker = null;
private UUID secondLastKicker = null; // für Assist-Erkennung private UUID secondLastKicker = null; // für Assist-Erkennung
private boolean lastKickWasHeader = false; // für Rückpass-Regel (Header erlaubt) private boolean lastKickWasHeader = false; // für Rückpass-Regel (Header erlaubt)
/** true wenn der letzte Schuss ein Restart-Kick war (Einwurf/Eckstoß/Abstoß/Anstoß)
* → der NÄCHSTE Empfänger darf lt. Regel 11 §3 nicht auf Abseits geprüft werden */
private boolean lastKickWasRestart = false;
private Team lastTouchTeam = null; private Team lastTouchTeam = null;
private Team throwInTeam = null; private Team throwInTeam = null;
@@ -389,6 +392,10 @@ public class Game {
cd--; cd--;
} else { } else {
spawnBallDelayed(arena.getBallSpawn()); spawnBallDelayed(arena.getBallSpawn());
// Regel 8: Rot führt den Anstoß in der 1. Halbzeit aus
throwInTeam = Team.RED;
kickoffEnforceTicks = 200; // 10s Kreisschutz (Regel 8: 9,15m Abstand)
broadcastAll(Messages.get("kickoff-team", "team", "§cRotes Team"));
for (UUID uuid : allPlayers) { for (UUID uuid : allPlayers) {
Player p = Bukkit.getPlayer(uuid); Player p = Bukkit.getPlayer(uuid);
if (p != null) { if (p != null) {
@@ -444,9 +451,19 @@ public class Game {
secondHalf = true; secondHalf = true;
timeLeft = arena.getGameDuration() / 2; timeLeft = arena.getGameDuration() / 2;
updateGoalBeaconColors(); updateGoalBeaconColors();
// Nachspielzeit der 1. Halbzeit zurücksetzen // Nachspielzeit und Spielzustand der 1. Halbzeit zurücksetzen
injuryTimeBuffer = 0; injuryTimeBuffer = 0;
inInjuryTime = false; inInjuryTime = false;
lastKicker = null;
secondLastKicker = null;
lastTouchTeam = null;
lastKickWasHeader = false;
lastKickWasRestart = false;
lastBallLocation = null;
outCooldown = false;
offsideCooldown = false;
headerCooldowns.clear();
outOfBoundsCountdown.clear();
// Seitenwechsel: Rotes Team → BlueSpawn, Blaues Team → RedSpawn // Seitenwechsel: Rotes Team → BlueSpawn, Blaues Team → RedSpawn
for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getBlueSpawn()); } for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getBlueSpawn()); }
@@ -472,6 +489,10 @@ public class Game {
cd--; cd--;
} else { } else {
spawnBallDelayed(arena.getBallSpawn()); spawnBallDelayed(arena.getBallSpawn());
// Regel 8: Das andere Team (Blau) stößt in der 2. Halbzeit an
throwInTeam = Team.BLUE;
kickoffEnforceTicks = 200;
broadcastAll(Messages.get("kickoff-team", "team", "§9Blaues Team"));
for (UUID uuid : allPlayers) { for (UUID uuid : allPlayers) {
Player p = Bukkit.getPlayer(uuid); Player p = Bukkit.getPlayer(uuid);
if (p != null) { if (p != null) {
@@ -497,11 +518,16 @@ public class Game {
if (ball != null) ball.remove(); if (ball != null) ball.remove();
spawnBallDelayed(arena.getBallSpawn()); spawnBallDelayed(arena.getBallSpawn());
// Regel 8: In der Verlängerung stößt das Team an, das in der 2. Halbzeit NICHT angestoßen hat.
// In der 2. HZ stieß Blau an → in der VL stößt Rot an.
throwInTeam = Team.RED;
kickoffEnforceTicks = 200; // 10s Anstoß-Kreis
broadcastAll("§6§l╔══════════════════════╗"); broadcastAll("§6§l╔══════════════════════╗");
broadcastAll("§6§l║ ⚽ VERLÄNGERUNG! ║"); broadcastAll("§6§l║ ⚽ VERLÄNGERUNG! ║");
broadcastAll("§6§l╚══════════════════════╝"); broadcastAll("§6§l╚══════════════════════╝");
broadcastAll("§7Spielstand: §c" + redScore + " §7: §9" + blueScore); broadcastAll("§7Spielstand: §c" + redScore + " §7: §9" + blueScore);
broadcastAll(Messages.get("kickoff-team", "team", "§cRotes Team"));
for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getRedSpawn()); p.sendTitle("§6§lVERLÄNGERUNG!", "§710 Minuten extra!", 10, 60, 10); } } for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getRedSpawn()); p.sendTitle("§6§lVERLÄNGERUNG!", "§710 Minuten extra!", 10, 60, 10); } }
for (UUID uuid : blueTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getBlueSpawn()); p.sendTitle("§6§lVERLÄNGERUNG!", "§710 Minuten extra!", 10, 60, 10); } } for (UUID uuid : blueTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getBlueSpawn()); p.sendTitle("§6§lVERLÄNGERUNG!", "§710 Minuten extra!", 10, 60, 10); } }
@@ -856,17 +882,27 @@ public class Game {
Vector diff = to.toVector().subtract(from.toVector()); Vector diff = to.toVector().subtract(from.toVector());
double distance = diff.length(); double distance = diff.length();
int steps = Math.max(1, (int) Math.ceil(distance / 0.2)); int steps = Math.max(1, (int) Math.ceil(distance / 0.2));
// BUG FIX: Vier Y-Offsets prüfen:
// 0.0 = ArmorStand-Füße (Entity-Position)
// 0.5 = Mitte des kleinen Stands
// 0.975 = tatsächliche Helmposition (Textur sichtbar hier!)
// 1.4 = konservativer oberer Puffer
// Früher wurden nur 0 und 1.4 geprüft → Bälle auf Helm-Höhe (0.975)
// wurden nicht als Tor erkannt und landeten als Ecke/Abstoß.
final double[] Y_OFFSETS = {0.0, 0.5, 0.975, 1.4};
for (int i = 0; i <= steps; i++) { for (int i = 0; i <= steps; i++) {
double t = (double) i / steps; double t = (double) i / steps;
Location p = from.clone().add(diff.clone().multiply(t)); Location base = from.clone().add(diff.clone().multiply(t));
Location head = p.clone().add(0, 1.4, 0); for (double dy : Y_OFFSETS) {
Location check = base.clone().add(0, dy, 0);
// In der 2. Halbzeit sind die Seiten getauscht → Tore umkehren // In der 2. Halbzeit sind die Seiten getauscht → Tore umkehren
if (!secondHalf) { if (!secondHalf) {
if (arena.isInRedGoal(p) || arena.isInRedGoal(head)) return Team.BLUE; if (arena.isInRedGoal(check)) return Team.BLUE;
if (arena.isInBlueGoal(p) || arena.isInBlueGoal(head)) return Team.RED; if (arena.isInBlueGoal(check)) return Team.RED;
} else { } else {
if (arena.isInRedGoal(p) || arena.isInRedGoal(head)) return Team.RED; if (arena.isInRedGoal(check)) return Team.RED;
if (arena.isInBlueGoal(p) || arena.isInBlueGoal(head)) return Team.BLUE; if (arena.isInBlueGoal(check)) return Team.BLUE;
}
} }
} }
return null; return null;
@@ -910,25 +946,47 @@ public class Game {
} }
} }
case "redEnd" -> { case "redEnd" -> {
if (touchTeam == Team.RED) { // Korrekte Fußball-Regel:
resumeLocation = moveInsideField(getCornerLocation(outLocation), 1.25); // Letzter Kontakt durch VERTEIDIGER an eigener Torlinie → ECKE für Angreifer
throwInTeam = Team.BLUE; // Letzter Kontakt durch ANGREIFER an gegnerischer Torlinie → ABSTOSS für Verteidiger
message = "§e⚽ §7Ball im Aus! §9Ecke für Blaues Team§7!"; // 1. Halbzeit: ROT verteidigt diese Seite (redEnd = rotes Tor)
// 2. Halbzeit: BLAU verteidigt diese Seite (Seitenwechsel)
Team defenderHere = secondHalf ? Team.BLUE : Team.RED;
Team attackerHere = defenderHere.getOpponent();
if (touchTeam == defenderHere) {
// Verteidiger hat den Ball ins Aus geschossen → ECKE für Angreifer
// Ball an Strafraumgrenze (11m-Linie) platzieren nicht in der Spielfeldecke
resumeLocation = moveInsideField(getPenaltyAreaCornerLocation(outLocation, true), 1.25);
throwInTeam = attackerHere;
String teamStr = attackerHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
message = "§e⚽ §7Ball im Aus! §7Ecke für " + teamStr + "§7!";
} else { } else {
resumeLocation = moveInsideField(arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn(), 1.25); // Angreifer (oder unbekannt) hat den Ball ins Aus geschossen → ABSTOSS für Verteidiger
throwInTeam = Team.RED; // Ball ~5,5 Blöcke vor der Torlinie (5-Meter-Raum), Feldmitte
message = "§e⚽ §7Ball im Aus! §cAbstoß für Rotes Team§7!"; resumeLocation = moveInsideField(getGoalKickSpawnLocation(true), 1.25);
throwInTeam = defenderHere;
String teamStr = defenderHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
message = "§e⚽ §7Ball im Aus! §7Abstoß für " + teamStr + "§7!";
} }
} }
case "blueEnd" -> { case "blueEnd" -> {
if (touchTeam == Team.BLUE) { // analog zu redEnd.
resumeLocation = moveInsideField(getCornerLocation(outLocation), 1.25); // 1. Halbzeit: BLAU verteidigt diese Seite (blueEnd = blaues Tor)
throwInTeam = Team.RED; // 2. Halbzeit: ROT verteidigt diese Seite (Seitenwechsel)
message = "§e⚽ §7Ball im Aus! §cEcke für Rotes Team§7!"; Team defenderHere = secondHalf ? Team.RED : Team.BLUE;
Team attackerHere = defenderHere.getOpponent();
if (touchTeam == defenderHere) {
// Verteidiger → ECKE für Angreifer
resumeLocation = moveInsideField(getPenaltyAreaCornerLocation(outLocation, false), 1.25);
throwInTeam = attackerHere;
String teamStr = attackerHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
message = "§e⚽ §7Ball im Aus! §7Ecke für " + teamStr + "§7!";
} else { } else {
resumeLocation = moveInsideField(arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn(), 1.25); // Angreifer (oder unbekannt) → ABSTOSS für Verteidiger
throwInTeam = Team.BLUE; resumeLocation = moveInsideField(getGoalKickSpawnLocation(false), 1.25);
message = "§e⚽ §7Ball im Aus! §9Abstoß für Blaues Team§7!"; throwInTeam = defenderHere;
String teamStr = defenderHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
message = "§e⚽ §7Ball im Aus! §7Abstoß für " + teamStr + "§7!";
} }
} }
default -> { default -> {
@@ -942,14 +1000,137 @@ public class Game {
for (UUID uuid : allPlayers) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 1f, 1.2f); } 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)); addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-out", 3));
// throwInTeam sichern spawnBallDelayed() würde es auf null zurücksetzen
final Team capturedThrowIn = throwInTeam;
final Location spawnHere = resumeLocation; final Location spawnHere = resumeLocation;
new BukkitRunnable() { new BukkitRunnable() {
public void run() { public void run() {
if (state == GameState.RUNNING || state == GameState.OVERTIME) spawnBallDelayed(spawnHere); if (state == GameState.RUNNING || state == GameState.OVERTIME) {
spawnBallDelayed(spawnHere);
throwInTeam = capturedThrowIn;
// Abstandsregel pro Spielfortsetzungs-Typ erzwingen (Regel 15/16/17)
freekickLocation = spawnHere.clone();
freekickTicks = plugin.getConfig().getInt("gameplay.freekick-duration", 600);
}
} }
}.runTaskLater(plugin, 40L); }.runTaskLater(plugin, 40L);
} }
/**
* Berechnet den Freistoß-Aufstellungsort für eine Ecke.
* Statt der wörtlichen Spielfeldecke wird der Ball an der Strafraumgrenze
* (Tiefe aus gameplay.penalty-area-depth) entlang der nächsten Seitenlinie platziert.
* Das entspricht dem Wunsch "an oder vor der 11-Meter-Grenze".
*
* @param outLoc Wo der Ball das Feld verlassen hat
* @param isRedEnd true = rotes Tor-Ende (redGoal-Seite des Feldes)
*/
private Location getPenaltyAreaCornerLocation(Location outLoc, boolean isRedEnd) {
if (arena.getFieldMin() == null || arena.getFieldMax() == null || arena.getBallSpawn() == null) {
return getCornerLocation(outLoc);
}
double y = arena.getBallSpawn().getY();
double penaltyDepth = plugin.getConfig().getDouble("gameplay.penalty-area-depth", 16);
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
double maxX = Math.max(arena.getFieldMin().getX(), arena.getFieldMax().getX());
double minZ = Math.min(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
double maxZ = Math.max(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
org.bukkit.util.Vector fieldDir = arena.getFieldDirection();
if (fieldDir == null) return getCornerLocation(outLoc);
if (Math.abs(fieldDir.getZ()) >= Math.abs(fieldDir.getX())) {
// ── Feld läuft entlang Z-Achse ──────────────────────────────────
boolean redIsLowZ = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
// Nächste Seitenlinie (X-Seite) bestimmen
double sideX = (Math.abs(outLoc.getX() - minX) <= Math.abs(outLoc.getX() - maxX)) ? minX : maxX;
// Z-Position: Strafraum-Tiefe vom Toraus-Ende ins Feld
double endZ, targetZ;
if (isRedEnd) {
endZ = redIsLowZ ? minZ : maxZ;
targetZ = redIsLowZ ? endZ + penaltyDepth : endZ - penaltyDepth;
} else {
endZ = redIsLowZ ? maxZ : minZ;
targetZ = redIsLowZ ? endZ - penaltyDepth : endZ + penaltyDepth;
}
targetZ = Math.max(minZ + 1.0, Math.min(maxZ - 1.0, targetZ));
return new Location(outLoc.getWorld(), sideX, y, targetZ);
} else {
// ── Feld läuft entlang X-Achse ──────────────────────────────────
boolean redIsLowX = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
double sideZ = (Math.abs(outLoc.getZ() - minZ) <= Math.abs(outLoc.getZ() - maxZ)) ? minZ : maxZ;
double endX, targetX;
if (isRedEnd) {
endX = redIsLowX ? minX : maxX;
targetX = redIsLowX ? endX + penaltyDepth : endX - penaltyDepth;
} else {
endX = redIsLowX ? maxX : minX;
targetX = redIsLowX ? endX - penaltyDepth : endX + penaltyDepth;
}
targetX = Math.max(minX + 1.0, Math.min(maxX - 1.0, targetX));
return new Location(outLoc.getWorld(), targetX, y, sideZ);
}
}
/**
* Gibt den Ball-Aufstellungsort für einen Abstoß zurück.
* Der Ball wird ~5.5 Blöcke vor der Torlinie, mittig auf dem Feld platziert
* (entspricht dem 5-Meter-Raum / Torabstoß-Raum im echten Fußball).
*
* @param isRedEnd true = roter Torbereich
*/
private Location getGoalKickSpawnLocation(boolean isRedEnd) {
if (arena.getFieldMin() == null || arena.getFieldMax() == null || arena.getBallSpawn() == null) {
if (isRedEnd) return arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn();
else return arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn();
}
double y = arena.getBallSpawn().getY();
final double GOAL_KICK_INSET = 5.5; // ~5-Meter-Raum (6-Yard-Box)
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
double maxX = Math.max(arena.getFieldMin().getX(), arena.getFieldMax().getX());
double minZ = Math.min(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
double maxZ = Math.max(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
double centerX = (minX + maxX) / 2.0;
double centerZ = (minZ + maxZ) / 2.0;
org.bukkit.util.Vector fieldDir = arena.getFieldDirection();
if (fieldDir == null) {
return isRedEnd ? (arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn())
: (arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn());
}
if (Math.abs(fieldDir.getZ()) >= Math.abs(fieldDir.getX())) {
// Feld entlang Z-Achse
boolean redIsLowZ = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
double kickZ;
if (isRedEnd) {
double endZ = redIsLowZ ? minZ : maxZ;
kickZ = redIsLowZ ? endZ + GOAL_KICK_INSET : endZ - GOAL_KICK_INSET;
} else {
double endZ = redIsLowZ ? maxZ : minZ;
kickZ = redIsLowZ ? endZ - GOAL_KICK_INSET : endZ + GOAL_KICK_INSET;
}
kickZ = Math.max(minZ + 1.0, Math.min(maxZ - 1.0, kickZ));
return new Location(arena.getFieldMin().getWorld(), centerX, y, kickZ);
} else {
// Feld entlang X-Achse
boolean redIsLowX = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
double kickX;
if (isRedEnd) {
double endX = redIsLowX ? minX : maxX;
kickX = redIsLowX ? endX + GOAL_KICK_INSET : endX - GOAL_KICK_INSET;
} else {
double endX = redIsLowX ? maxX : minX;
kickX = redIsLowX ? endX - GOAL_KICK_INSET : endX + GOAL_KICK_INSET;
}
kickX = Math.max(minX + 1.0, Math.min(maxX - 1.0, kickX));
return new Location(arena.getFieldMin().getWorld(), kickX, y, centerZ);
}
}
/** Hilfsmethode: wörtliche Spielfeldecke (als Fallback). */
private Location getCornerLocation(Location outLoc) { private Location getCornerLocation(Location outLoc) {
if (arena.getFieldMin() == null || arena.getFieldMax() == null) return arena.getBallSpawn(); if (arena.getFieldMin() == null || arena.getFieldMax() == null) return arena.getBallSpawn();
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX()); double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
@@ -1019,8 +1200,8 @@ public class Game {
} }
} }
throwInTeam = null; clearThrowIn(); // setzt lastKickWasRestart=true falls Einwurf/Restart war → kein Abseits für Empfänger (Regel 11 §3)
setLastKicker(uuid); // korrekt: nutzt setLastKicker statt direktem Feldzugriff setLastKicker(uuid);
ball.kick(p); ball.kick(p);
break; // pro Tick max. 1 Auto-Kick break; // pro Tick max. 1 Auto-Kick
} }
@@ -1370,30 +1551,43 @@ public class Game {
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-foul", 5)); addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-foul", 5));
logMatchEvent("§cFoul: §e" + fouler.getName() + " §7→ §e" + victim.getName()); logMatchEvent("§cFoul: §e" + fouler.getName() + " §7→ §e" + victim.getName());
// ── Foul im Strafraum → Elfmeter ─────────────────────────────────── // ── Foul im Strafraum → Elfmeter (Regel 14) ──────────────────────────────
// Regel 14: Strafstoß wenn ein Spieler ein direktes Foul im eigenen Strafraum begeht.
// In der 2. Halbzeit sind die Seiten getauscht:
// 1. HZ: Rot verteidigt roten SR, Blau verteidigt blauen SR
// 2. HZ: Blau verteidigt roten SR, Rot verteidigt blauen SR
boolean inRedPenalty = arena.isInRedPenaltyArea(foulLocation); boolean inRedPenalty = arena.isInRedPenaltyArea(foulLocation);
boolean inBluePenalty = arena.isInBluePenaltyArea(foulLocation); boolean inBluePenalty = arena.isInBluePenaltyArea(foulLocation);
boolean penaltyKick = false; boolean penaltyKick = false;
if (inRedPenalty && victimTeam == Team.BLUE) { boolean penaltyForBlue, penaltyForRed;
// Foul an Blau im roten Strafraum → Elfmeter für Blau if (!secondHalf) {
penaltyForBlue = inRedPenalty && victimTeam == Team.BLUE;
penaltyForRed = inBluePenalty && victimTeam == Team.RED;
} else {
// Seitenwechsel: Blau greift jetzt auf roten SR-Seite an
penaltyForBlue = inBluePenalty && victimTeam == Team.BLUE;
penaltyForRed = inRedPenalty && victimTeam == Team.RED;
}
if (penaltyForBlue) {
broadcastAll(Messages.get("foul-penalty", "team", "§9Blaues Team")); broadcastAll(Messages.get("foul-penalty", "team", "§9Blaues Team"));
for (UUID uuid : getAllAndSpectators()) { for (UUID uuid : getAllAndSpectators()) {
Player p = Bukkit.getPlayer(uuid); Player p = Bukkit.getPlayer(uuid);
if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§9Blaues Team§7 schießt!", 5, 50, 10); if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§9Blaues Team§7 schießt!", 5, 50, 10);
} }
penaltyKick = true; penaltyKick = true;
// Elfmeter als Freistoß direkt auf Ballspawn (ggf. später: separater Elfmeter-Punkt) Location penSpot = arena.getPenaltySpot(Team.BLUE);
startFreekick(Team.BLUE, arena.getBallSpawn(), "Elfmeter"); startFreekick(Team.BLUE, penSpot != null ? penSpot : arena.getBallSpawn(), "Elfmeter");
} else if (inBluePenalty && victimTeam == Team.RED) { } else if (penaltyForRed) {
// Foul an Rot im blauen Strafraum → Elfmeter für Rot
broadcastAll(Messages.get("foul-penalty", "team", "§cRotes Team")); broadcastAll(Messages.get("foul-penalty", "team", "§cRotes Team"));
for (UUID uuid : getAllAndSpectators()) { for (UUID uuid : getAllAndSpectators()) {
Player p = Bukkit.getPlayer(uuid); Player p = Bukkit.getPlayer(uuid);
if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§cRotes Team§7 schießt!", 5, 50, 10); if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§cRotes Team§7 schießt!", 5, 50, 10);
} }
penaltyKick = true; penaltyKick = true;
startFreekick(Team.RED, arena.getBallSpawn(), "Elfmeter"); Location penSpot = arena.getPenaltySpot(Team.RED);
startFreekick(Team.RED, penSpot != null ? penSpot : arena.getBallSpawn(), "Elfmeter");
} }
if (!penaltyKick) { if (!penaltyKick) {
@@ -1447,8 +1641,18 @@ public class Game {
broadcastAll(Messages.get("freekick-hint", "n", String.format("%.0f", dist))); broadcastAll(Messages.get("freekick-hint", "n", String.format("%.0f", dist)));
} }
/**
* Erzwingt den korrekten Abstand für die jeweilige Spielfortsetzung (Regel 1317).
* Freistoß (Regel 13): 5 Blöcke (config: freekick-distance)
* Einwurf (Regel 15): 2 Blöcke
* Abstoß (Regel 16): 9,15 Blöcke (Gegner außerhalb Strafraum)
* Eckstoß (Regel 17): 9,15 Blöcke
* Anstoß (Regel 8): 9,15 Blöcke (via kickoffEnforceTicks)
*/
private void enforceFreekickDistance() { private void enforceFreekickDistance() {
if (freekickLocation == null || throwInTeam == null) return; if (freekickLocation == null || throwInTeam == null) return;
// Abstand je nach Typ: prüfe ob freekickLocation nah an einer Seitenlinie ist (=Einwurf)
// oder in der Feldhälfte nahe einer Torlinie (=Abstoß/Eckstoß) oder zentral (=Freistoß)
double minDist = plugin.getConfig().getDouble("gameplay.freekick-distance", 5.0); double minDist = plugin.getConfig().getDouble("gameplay.freekick-distance", 5.0);
Team opposingTeam = throwInTeam.getOpponent(); Team opposingTeam = throwInTeam.getOpponent();
List<UUID> opponents = opposingTeam == Team.RED ? redTeam : blueTeam; List<UUID> opponents = opposingTeam == Team.RED ? redTeam : blueTeam;
@@ -1843,6 +2047,7 @@ public class Game {
lastKicker = null; lastKicker = null;
secondLastKicker = null; secondLastKicker = null;
lastKickWasHeader = false; lastKickWasHeader = false;
lastKickWasRestart = false;
secondHalf = false; secondHalf = false;
updateGoalBeaconColors(); updateGoalBeaconColors();
@@ -2121,7 +2326,8 @@ public class Game {
Player newKicker = Bukkit.getPlayer(uuid); Player newKicker = Bukkit.getPlayer(uuid);
if (prevKicker != null && newKicker != null) { if (prevKicker != null && newKicker != null) {
double dist = lastKickLocation.distance(ball.getEntity().getLocation()); double dist = lastKickLocation.distance(ball.getEntity().getLocation());
if (dist >= LONG_PASS_DISTANCE && getTeam(prevKicker) == getTeam(newKicker)) { double longPassDist = plugin.getConfig().getDouble("gameplay.long-pass-distance", LONG_PASS_DISTANCE);
if (dist >= longPassDist && getTeam(prevKicker) == getTeam(newKicker)) {
// Langer Pass innerhalb des Teams // Langer Pass innerhalb des Teams
String msg = "§7⚽ §eLangpass §7von §f" + prevKicker.getName() String msg = "§7⚽ §eLangpass §7von §f" + prevKicker.getName()
+ " §7zu §f" + newKicker.getName() + " §7zu §f" + newKicker.getName()
@@ -2144,10 +2350,16 @@ public class Game {
if (p != null) lastTouchTeam = getTeam(p); if (p != null) lastTouchTeam = getTeam(p);
if (p != null) kicks.merge(uuid, 1, Integer::sum); if (p != null) kicks.merge(uuid, 1, Integer::sum);
// Abseits-Check // ── Abseits-Check (Regel 11 §3: kein Abseits nach Einwurf, Abstoß, Eckstoß) ──
// lastKickWasRestart wird in clearThrowIn() gesetzt wenn throwInTeam != null war.
// Der EMPFÄNGER des ersten Restart-Passes darf nicht auf Abseits geprüft werden.
// Das Flag wird hier konsumiert (→ gilt nur für diesen einen Empfänger).
boolean skipOffside = lastKickWasRestart;
lastKickWasRestart = false; // Flag zurücksetzen nach Konsum
if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true) if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true)
&& (state == GameState.RUNNING || state == GameState.OVERTIME) && (state == GameState.RUNNING || state == GameState.OVERTIME)
&& ball != null && ball.getEntity() != null && !offsideCooldown) { && ball != null && ball.getEntity() != null && !offsideCooldown
&& !skipOffside) {
checkOffside(uuid, ball.getEntity().getLocation()); checkOffside(uuid, ball.getEntity().getLocation());
} }
} }
@@ -2163,9 +2375,13 @@ public class Game {
if (p != null) lastTouchTeam = getTeam(p); if (p != null) lastTouchTeam = getTeam(p);
if (p != null) kicks.merge(uuid, 1, Integer::sum); if (p != null) kicks.merge(uuid, 1, Integer::sum);
// Kopfball: gleiche Abseits-Logik kein Abseits wenn Restart-Empfänger
boolean skipOffside = lastKickWasRestart;
lastKickWasRestart = false;
if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true) if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true)
&& (state == GameState.RUNNING || state == GameState.OVERTIME) && (state == GameState.RUNNING || state == GameState.OVERTIME)
&& ball != null && ball.getEntity() != null && !offsideCooldown) { && ball != null && ball.getEntity() != null && !offsideCooldown
&& !skipOffside) {
checkOffside(uuid, ball.getEntity().getLocation()); checkOffside(uuid, ball.getEntity().getLocation());
} }
} }
@@ -2201,6 +2417,8 @@ public class Game {
public boolean isLastKickWasHeader() { return lastKickWasHeader; } public boolean isLastKickWasHeader() { return lastKickWasHeader; }
/** Berechtigung aufheben wird von BallListener nach dem ersten Schuss gerufen */ /** Berechtigung aufheben wird von BallListener nach dem ersten Schuss gerufen */
public void clearThrowIn() { public void clearThrowIn() {
// Wenn throwInTeam gesetzt war, war das ein Restart-Kick → nächster Empfänger kein Abseits
if (throwInTeam != null) lastKickWasRestart = true;
throwInTeam = null; throwInTeam = null;
freekickLocation = null; freekickLocation = null;
freekickTicks = 0; freekickTicks = 0;

View File

@@ -17,6 +17,29 @@ import java.util.function.Consumer;
* new UpdateChecker(this, RESOURCE_ID).getVersion(version -> { ... }); * new UpdateChecker(this, RESOURCE_ID).getVersion(version -> { ... });
*/ */
public class UpdateChecker { public class UpdateChecker {
/**
* Vergleicht zwei Versionsnummern (z.B. "1.0.3" und "1.0.2").
* Gibt >0 zurück, wenn v1 > v2, <0 wenn v1 < v2, 0 wenn gleich.
*/
public static int compareVersions(String v1, String v2) {
String[] parts1 = v1.replace("v", "").split("\\.");
String[] parts2 = v2.replace("v", "").split("\\.");
int len = Math.max(parts1.length, parts2.length);
for (int i = 0; i < len; i++) {
int n1 = i < parts1.length ? parseIntSafe(parts1[i]) : 0;
int n2 = i < parts2.length ? parseIntSafe(parts2[i]) : 0;
if (n1 != n2) return Integer.compare(n1, n2);
}
return 0;
}
private static int parseIntSafe(String s) {
try {
return Integer.parseInt(s.replaceAll("[^0-9]", ""));
} catch (NumberFormatException e) {
return 0;
}
}
private final JavaPlugin plugin; private final JavaPlugin plugin;
private final int resourceId; private final int resourceId;