Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0085ba5bd | |||
| e10f1313bd | |||
| 960290dfa8 | |||
| 3f6d91cdc7 |
Binary file not shown.
@@ -78,7 +78,10 @@ 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 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 throwInTeam = null;
|
||||
|
||||
@@ -389,6 +392,10 @@ public class Game {
|
||||
cd--;
|
||||
} else {
|
||||
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) {
|
||||
Player p = Bukkit.getPlayer(uuid);
|
||||
if (p != null) {
|
||||
@@ -444,9 +451,19 @@ public class Game {
|
||||
secondHalf = true;
|
||||
timeLeft = arena.getGameDuration() / 2;
|
||||
updateGoalBeaconColors();
|
||||
// Nachspielzeit der 1. Halbzeit zurücksetzen
|
||||
injuryTimeBuffer = 0;
|
||||
inInjuryTime = false;
|
||||
// Nachspielzeit und Spielzustand der 1. Halbzeit zurücksetzen
|
||||
injuryTimeBuffer = 0;
|
||||
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
|
||||
for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getBlueSpawn()); }
|
||||
@@ -472,6 +489,10 @@ public class Game {
|
||||
cd--;
|
||||
} else {
|
||||
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) {
|
||||
Player p = Bukkit.getPlayer(uuid);
|
||||
if (p != null) {
|
||||
@@ -497,11 +518,16 @@ public class Game {
|
||||
|
||||
if (ball != null) ball.remove();
|
||||
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║ ⚽ VERLÄNGERUNG! ║");
|
||||
broadcastAll("§6§l╚══════════════════════╝");
|
||||
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 : 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());
|
||||
double distance = diff.length();
|
||||
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++) {
|
||||
double t = (double) i / steps;
|
||||
Location p = from.clone().add(diff.clone().multiply(t));
|
||||
Location head = p.clone().add(0, 1.4, 0);
|
||||
// In der 2. Halbzeit sind die Seiten getauscht → Tore umkehren
|
||||
if (!secondHalf) {
|
||||
if (arena.isInRedGoal(p) || arena.isInRedGoal(head)) return Team.BLUE;
|
||||
if (arena.isInBlueGoal(p) || arena.isInBlueGoal(head)) return Team.RED;
|
||||
} else {
|
||||
if (arena.isInRedGoal(p) || arena.isInRedGoal(head)) return Team.RED;
|
||||
if (arena.isInBlueGoal(p) || arena.isInBlueGoal(head)) return Team.BLUE;
|
||||
Location base = from.clone().add(diff.clone().multiply(t));
|
||||
for (double dy : Y_OFFSETS) {
|
||||
Location check = base.clone().add(0, dy, 0);
|
||||
// In der 2. Halbzeit sind die Seiten getauscht → Tore umkehren
|
||||
if (!secondHalf) {
|
||||
if (arena.isInRedGoal(check)) return Team.BLUE;
|
||||
if (arena.isInBlueGoal(check)) return Team.RED;
|
||||
} else {
|
||||
if (arena.isInRedGoal(check)) return Team.RED;
|
||||
if (arena.isInBlueGoal(check)) return Team.BLUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -910,25 +946,47 @@ public class Game {
|
||||
}
|
||||
}
|
||||
case "redEnd" -> {
|
||||
if (touchTeam == Team.RED) {
|
||||
resumeLocation = moveInsideField(getCornerLocation(outLocation), 1.25);
|
||||
throwInTeam = Team.BLUE;
|
||||
message = "§e⚽ §7Ball im Aus! §9Ecke für Blaues Team§7!";
|
||||
// Korrekte Fußball-Regel:
|
||||
// Letzter Kontakt durch VERTEIDIGER an eigener Torlinie → ECKE für Angreifer
|
||||
// Letzter Kontakt durch ANGREIFER an gegnerischer Torlinie → ABSTOSS für Verteidiger
|
||||
// 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 {
|
||||
resumeLocation = moveInsideField(arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn(), 1.25);
|
||||
throwInTeam = Team.RED;
|
||||
message = "§e⚽ §7Ball im Aus! §cAbstoß für Rotes Team§7!";
|
||||
// Angreifer (oder unbekannt) hat den Ball ins Aus geschossen → ABSTOSS für Verteidiger
|
||||
// Ball ~5,5 Blöcke vor der Torlinie (5-Meter-Raum), Feldmitte
|
||||
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" -> {
|
||||
if (touchTeam == Team.BLUE) {
|
||||
resumeLocation = moveInsideField(getCornerLocation(outLocation), 1.25);
|
||||
throwInTeam = Team.RED;
|
||||
message = "§e⚽ §7Ball im Aus! §cEcke für Rotes Team§7!";
|
||||
// analog zu redEnd.
|
||||
// 1. Halbzeit: BLAU verteidigt diese Seite (blueEnd = blaues Tor)
|
||||
// 2. Halbzeit: ROT verteidigt diese Seite (Seitenwechsel)
|
||||
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 {
|
||||
resumeLocation = moveInsideField(arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn(), 1.25);
|
||||
throwInTeam = Team.BLUE;
|
||||
message = "§e⚽ §7Ball im Aus! §9Abstoß für Blaues Team§7!";
|
||||
// Angreifer (oder unbekannt) → ABSTOSS für Verteidiger
|
||||
resumeLocation = moveInsideField(getGoalKickSpawnLocation(false), 1.25);
|
||||
throwInTeam = defenderHere;
|
||||
String teamStr = defenderHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
|
||||
message = "§e⚽ §7Ball im Aus! §7Abstoß für " + teamStr + "§7!";
|
||||
}
|
||||
}
|
||||
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); }
|
||||
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;
|
||||
new BukkitRunnable() {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (arena.getFieldMin() == null || arena.getFieldMax() == null) return arena.getBallSpawn();
|
||||
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
|
||||
@@ -1019,8 +1200,8 @@ public class Game {
|
||||
}
|
||||
}
|
||||
|
||||
throwInTeam = null;
|
||||
setLastKicker(uuid); // korrekt: nutzt setLastKicker statt direktem Feldzugriff
|
||||
clearThrowIn(); // setzt lastKickWasRestart=true falls Einwurf/Restart war → kein Abseits für Empfänger (Regel 11 §3)
|
||||
setLastKicker(uuid);
|
||||
ball.kick(p);
|
||||
break; // pro Tick max. 1 Auto-Kick
|
||||
}
|
||||
@@ -1370,30 +1551,43 @@ public class Game {
|
||||
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-foul", 5));
|
||||
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 inBluePenalty = arena.isInBluePenaltyArea(foulLocation);
|
||||
boolean penaltyKick = false;
|
||||
|
||||
if (inRedPenalty && victimTeam == Team.BLUE) {
|
||||
// Foul an Blau im roten Strafraum → Elfmeter für Blau
|
||||
boolean penaltyForBlue, penaltyForRed;
|
||||
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"));
|
||||
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
|
||||
Location penSpot = arena.getPenaltySpot(Team.BLUE);
|
||||
startFreekick(Team.BLUE, penSpot != null ? penSpot : arena.getBallSpawn(), "Elfmeter");
|
||||
} else if (penaltyForRed) {
|
||||
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");
|
||||
Location penSpot = arena.getPenaltySpot(Team.RED);
|
||||
startFreekick(Team.RED, penSpot != null ? penSpot : arena.getBallSpawn(), "Elfmeter");
|
||||
}
|
||||
|
||||
if (!penaltyKick) {
|
||||
@@ -1447,8 +1641,18 @@ public class Game {
|
||||
broadcastAll(Messages.get("freekick-hint", "n", String.format("%.0f", dist)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzwingt den korrekten Abstand für die jeweilige Spielfortsetzung (Regel 13–17).
|
||||
* 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() {
|
||||
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);
|
||||
Team opposingTeam = throwInTeam.getOpponent();
|
||||
List<UUID> opponents = opposingTeam == Team.RED ? redTeam : blueTeam;
|
||||
@@ -1840,10 +2044,11 @@ public class Game {
|
||||
throwInTeam = null;
|
||||
injuryTimeBuffer = 0;
|
||||
inInjuryTime = false;
|
||||
lastKicker = null;
|
||||
secondLastKicker = null;
|
||||
lastKickWasHeader = false;
|
||||
secondHalf = false;
|
||||
lastKicker = null;
|
||||
secondLastKicker = null;
|
||||
lastKickWasHeader = false;
|
||||
lastKickWasRestart = false;
|
||||
secondHalf = false;
|
||||
updateGoalBeaconColors();
|
||||
|
||||
// Persistente Statistiken speichern
|
||||
@@ -2121,7 +2326,8 @@ public class Game {
|
||||
Player newKicker = Bukkit.getPlayer(uuid);
|
||||
if (prevKicker != null && newKicker != null) {
|
||||
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
|
||||
String msg = "§7⚽ §eLangpass §7von §f" + prevKicker.getName()
|
||||
+ " §7zu §f" + newKicker.getName()
|
||||
@@ -2144,10 +2350,16 @@ public class Game {
|
||||
if (p != null) lastTouchTeam = getTeam(p);
|
||||
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)
|
||||
&& (state == GameState.RUNNING || state == GameState.OVERTIME)
|
||||
&& ball != null && ball.getEntity() != null && !offsideCooldown) {
|
||||
&& ball != null && ball.getEntity() != null && !offsideCooldown
|
||||
&& !skipOffside) {
|
||||
checkOffside(uuid, ball.getEntity().getLocation());
|
||||
}
|
||||
}
|
||||
@@ -2163,9 +2375,13 @@ public class Game {
|
||||
if (p != null) lastTouchTeam = getTeam(p);
|
||||
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)
|
||||
&& (state == GameState.RUNNING || state == GameState.OVERTIME)
|
||||
&& ball != null && ball.getEntity() != null && !offsideCooldown) {
|
||||
&& ball != null && ball.getEntity() != null && !offsideCooldown
|
||||
&& !skipOffside) {
|
||||
checkOffside(uuid, ball.getEntity().getLocation());
|
||||
}
|
||||
}
|
||||
@@ -2201,6 +2417,8 @@ public class Game {
|
||||
public boolean isLastKickWasHeader() { return lastKickWasHeader; }
|
||||
/** Berechtigung aufheben – wird von BallListener nach dem ersten Schuss gerufen */
|
||||
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;
|
||||
freekickLocation = null;
|
||||
freekickTicks = 0;
|
||||
|
||||
@@ -17,6 +17,29 @@ import java.util.function.Consumer;
|
||||
* new UpdateChecker(this, RESOURCE_ID).getVersion(version -> { ... });
|
||||
*/
|
||||
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 int resourceId;
|
||||
|
||||
Reference in New Issue
Block a user