package de.fussball.plugin.arena; import de.fussball.plugin.Fussball; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.configuration.serialization.ConfigurationSerializable; import java.util.*; public class Arena implements ConfigurationSerializable { private final String name; 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; // Elfmeter-Punkte (optional – sonst wird ballSpawn genutzt) private Location redPenaltySpot, bluePenaltySpot; private int minPlayers, maxPlayers, gameDuration; /** * Neue Arena erstellt – Standardwerte werden aus der config.yml gelesen. * Bereits gespeicherte Arenen laden ihre eigenen Werte über deserialize(). */ public Arena(String name) { this.name = name; Fussball plugin = Fussball.getInstance(); if (plugin != null) { this.minPlayers = plugin.getConfig().getInt("defaults.min-players", 2); this.maxPlayers = plugin.getConfig().getInt("defaults.max-players", 10); this.gameDuration = plugin.getConfig().getInt("defaults.game-duration", 300); } else { // Fallback falls getInstance() noch nicht verfügbar (z.B. Deserialisierung) this.minPlayers = 2; this.maxPlayers = 10; this.gameDuration = 300; } } public boolean isSetupComplete() { return center != null && redSpawn != null && blueSpawn != null && ballSpawn != null && redGoalMin != null && redGoalMax != null && blueGoalMin != null && blueGoalMax != null && lobby != null; } 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 * als Aus erkannt wurde, weil der ArmorStand über dem Boden schwebt. */ public boolean isInField(Location loc) { if (fieldMin == null || fieldMax == null) return true; return isInField2D(loc); } private boolean isInField2D(Location loc) { if (fieldMin == null || fieldMax == null || loc == null) return true; double minX = Math.min(fieldMin.getX(), fieldMax.getX()); double maxX = Math.max(fieldMin.getX(), fieldMax.getX()); double minZ = Math.min(fieldMin.getZ(), fieldMax.getZ()); double maxZ = Math.max(fieldMin.getZ(), fieldMax.getZ()); return loc.getX() >= minX && loc.getX() <= maxX && loc.getZ() >= minZ && loc.getZ() <= maxZ; } /** Auf welcher Seite hat der Ball das Feld verlassen? (nur XZ, kein Y) */ public String getOutSide(Location loc) { if (fieldMin == null || fieldMax == null) return null; if (isInField2D(loc)) return null; double minX = Math.min(fieldMin.getX(), fieldMax.getX()); double maxX = Math.max(fieldMin.getX(), fieldMax.getX()); double minZ = Math.min(fieldMin.getZ(), fieldMax.getZ()); double maxZ = Math.max(fieldMin.getZ(), fieldMax.getZ()); double lenX = maxX - minX; double lenZ = maxZ - minZ; if (lenZ >= lenX) { if (loc.getZ() < minZ) return "redEnd"; if (loc.getZ() > maxZ) return "blueEnd"; return "side"; } else { if (loc.getX() < minX) return "redEnd"; if (loc.getX() > maxX) return "blueEnd"; return "side"; } } public Location clampToField(Location loc) { if (fieldMin == null || fieldMax == null) return loc; double x = Math.max(Math.min(fieldMin.getX(), fieldMax.getX()), Math.min(loc.getX(), Math.max(fieldMin.getX(), fieldMax.getX()))); double y = loc.getY(); double z = Math.max(Math.min(fieldMin.getZ(), fieldMax.getZ()), Math.min(loc.getZ(), Math.max(fieldMin.getZ(), fieldMax.getZ()))); return new Location(loc.getWorld(), x, y, z); } // Tor-Erkennung: volle 3D-Prüfung (Y ist für das Tor wichtig!) private boolean isInRegion(Location loc, Location min, Location max) { if (min == null || max == null || loc == null) return false; if (loc.getWorld() == null || min.getWorld() == null) return false; if (!loc.getWorld().equals(min.getWorld())) return false; return loc.getX() >= Math.min(min.getX(), max.getX()) && loc.getX() <= Math.max(min.getX(), max.getX()) && loc.getY() >= Math.min(min.getY(), max.getY()) && loc.getY() <= Math.max(min.getY(), max.getY()) && loc.getZ() >= Math.min(min.getZ(), max.getZ()) && loc.getZ() <= Math.max(min.getZ(), max.getZ()); } // ── Spielfeld-Achse (für Abseits-Berechnung) ──────────────────────────── /** * Gibt den Einheitsvektor zurück der vom roten Tor zum blauen Tor zeigt. * Funktioniert unabhängig davon ob das Feld in X- oder Z-Richtung ausgerichtet ist. */ public org.bukkit.util.Vector getFieldDirection() { if (redGoalMin == null || redGoalMax == null || blueGoalMin == null || blueGoalMax == null) return null; double rX = (redGoalMin.getX() + redGoalMax.getX()) / 2.0; double rZ = (redGoalMin.getZ() + redGoalMax.getZ()) / 2.0; double bX = (blueGoalMin.getX() + blueGoalMax.getX()) / 2.0; double bZ = (blueGoalMin.getZ() + blueGoalMax.getZ()) / 2.0; org.bukkit.util.Vector dir = new org.bukkit.util.Vector(bX - rX, 0, bZ - rZ); double len = dir.length(); if (len < 0.001) return null; return dir.multiply(1.0 / len); } /** Projektionswert einer Location auf die Spielfeld-Achse */ public double getAxisValue(org.bukkit.Location loc) { org.bukkit.util.Vector dir = getFieldDirection(); if (dir == null || loc == null) return 0; return loc.getX() * dir.getX() + loc.getZ() * dir.getZ(); } public double getRedGoalAxisValue() { if (redGoalMin == null || redGoalMax == null) return 0; org.bukkit.util.Vector dir = getFieldDirection(); if (dir == null) return 0; double cx = (redGoalMin.getX() + redGoalMax.getX()) / 2.0; double cz = (redGoalMin.getZ() + redGoalMax.getZ()) / 2.0; return cx * dir.getX() + cz * dir.getZ(); } public double getBlueGoalAxisValue() { if (blueGoalMin == null || blueGoalMax == null) return 0; org.bukkit.util.Vector dir = getFieldDirection(); if (dir == null) return 0; double cx = (blueGoalMin.getX() + blueGoalMax.getX()) / 2.0; double cz = (blueGoalMin.getZ() + blueGoalMax.getZ()) / 2.0; return cx * dir.getX() + cz * dir.getZ(); } public double getCenterAxisValue() { return (getRedGoalAxisValue() + getBlueGoalAxisValue()) / 2.0; } // ── Serialisierung ─────────────────────────────────────────────────────── @Override public Map serialize() { Map map = new LinkedHashMap<>(); map.put("name", name); map.put("minPlayers", minPlayers); map.put("maxPlayers", maxPlayers); map.put("gameDuration", gameDuration); if (lobby != null) map.put("lobby", serLoc(lobby)); if (center != null) map.put("center", serLoc(center)); if (redSpawn != null) map.put("redSpawn", serLoc(redSpawn)); if (blueSpawn != null) map.put("blueSpawn", serLoc(blueSpawn)); if (ballSpawn != null) map.put("ballSpawn", serLoc(ballSpawn)); if (redGoalMin != null) map.put("redGoalMin", serLoc(redGoalMin)); if (redGoalMax != null) map.put("redGoalMax", serLoc(redGoalMax)); if (blueGoalMin != null) map.put("blueGoalMin", serLoc(blueGoalMin)); 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)); if (redPenaltySpot != null) map.put("redPenaltySpot", serLoc(redPenaltySpot)); if (bluePenaltySpot != null) map.put("bluePenaltySpot", serLoc(bluePenaltySpot)); return map; } /** * Beim Laden gespeicherter Arenen werden die eigenen Werte aus der YAML-Datei * gelesen – NICHT aus der config.yml. So kann jede Arena eigene Werte haben. */ public static Arena deserialize(Map map) { Arena a = new Arena((String) map.get("name")); // Überschreibe Konstruktor-Defaults mit den gespeicherten Werten a.minPlayers = getInt(map, "minPlayers", a.minPlayers); a.maxPlayers = getInt(map, "maxPlayers", a.maxPlayers); a.gameDuration = getInt(map, "gameDuration", a.gameDuration); if (map.containsKey("lobby")) a.lobby = desLoc(map.get("lobby")); if (map.containsKey("center")) a.center = desLoc(map.get("center")); if (map.containsKey("redSpawn")) a.redSpawn = desLoc(map.get("redSpawn")); if (map.containsKey("blueSpawn")) a.blueSpawn = desLoc(map.get("blueSpawn")); if (map.containsKey("ballSpawn")) a.ballSpawn = desLoc(map.get("ballSpawn")); if (map.containsKey("redGoalMin")) a.redGoalMin = desLoc(map.get("redGoalMin")); if (map.containsKey("redGoalMax")) a.redGoalMax = desLoc(map.get("redGoalMax")); if (map.containsKey("blueGoalMin")) a.blueGoalMin = desLoc(map.get("blueGoalMin")); 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")); if (map.containsKey("redPenaltySpot")) a.redPenaltySpot = desLoc(map.get("redPenaltySpot")); if (map.containsKey("bluePenaltySpot")) a.bluePenaltySpot = desLoc(map.get("bluePenaltySpot")); return a; } private static String serLoc(Location l) { return l.getWorld().getName() + ";" + l.getX() + ";" + l.getY() + ";" + l.getZ() + ";" + l.getYaw() + ";" + l.getPitch(); } private static Location desLoc(Object obj) { if (obj == null) return null; try { String[] p = obj.toString().split(";"); World world = Bukkit.getWorld(p[0]); if (world == null) return null; return new Location(world, Double.parseDouble(p[1]), Double.parseDouble(p[2]), Double.parseDouble(p[3]), p.length > 4 ? Float.parseFloat(p[4]) : 0f, p.length > 5 ? Float.parseFloat(p[5]) : 0f); } catch (Exception e) { return null; } } private static int getInt(Map map, String key, int def) { Object v = map.get(key); return v instanceof Number ? ((Number) v).intValue() : def; } // ── Getter / Setter ────────────────────────────────────────────────────── public String getName() { return name; } public Location getCenter() { return center; } public void setCenter(Location l) { this.center = l; } public Location getRedSpawn() { return redSpawn; } public void setRedSpawn(Location l) { this.redSpawn = l; } public Location getBlueSpawn() { return blueSpawn; } public void setBlueSpawn(Location l) { this.blueSpawn = l; } public Location getBallSpawn() { return ballSpawn; } public void setBallSpawn(Location l) { this.ballSpawn = l; } public Location getRedGoalMin() { return redGoalMin; } public void setRedGoalMin(Location l) { this.redGoalMin = l; } public Location getRedGoalMax() { return redGoalMax; } public void setRedGoalMax(Location l) { this.redGoalMax = l; } public Location getBlueGoalMin() { return blueGoalMin; } public void setBlueGoalMin(Location l) { this.blueGoalMin = l; } public Location getBlueGoalMax() { return blueGoalMax; } public void setBlueGoalMax(Location l) { this.blueGoalMax = l; } public Location getLobby() { return lobby; } public void setLobby(Location l) { this.lobby = l; } public Location getFieldMin() { return fieldMin; } 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; } // ── Elfmeter-Punkte ────────────────────────────────────────────────────── public Location getRedPenaltySpot() { return redPenaltySpot; } public void setRedPenaltySpot(Location l) { this.redPenaltySpot = l; } public Location getBluePenaltySpot() { return bluePenaltySpot; } public void setBluePenaltySpot(Location l) { this.bluePenaltySpot = l; } /** * Gibt den Elfmeter-Punkt für ein Team zurück. * Falls nicht gesetzt, wird ballSpawn als Fallback verwendet. */ public Location getPenaltySpot(de.fussball.plugin.game.Team team) { if (team == de.fussball.plugin.game.Team.RED) { return redPenaltySpot != null ? redPenaltySpot : ballSpawn; } else { return bluePenaltySpot != null ? bluePenaltySpot : ballSpawn; } } public boolean hasManualPenaltyAreas() { 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; } public void setMaxPlayers(int n) { this.maxPlayers = n; } public int getGameDuration() { return gameDuration; } public void setGameDuration(int n) { this.gameDuration = n; } }