diff --git a/src/main/java/de/fussball/plugin/game/Game.java b/src/main/java/de/fussball/plugin/game/Game.java index bc7afa1..44b2499 100644 --- a/src/main/java/de/fussball/plugin/game/Game.java +++ b/src/main/java/de/fussball/plugin/game/Game.java @@ -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 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; diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index cd0afc2..813bff3 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: Fussball -version: 1.0.3 +version: 1.0.4 main: de.fussball.plugin.Fussball api-version: 1.21 author: M_Viper