From e718d06473f2c2767fb62144fcca2825ac9bcbe1 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Fri, 27 Feb 2026 00:44:25 +0100 Subject: [PATCH] Update from Git Manager GUI --- .../java/de/fussball/plugin/Fussball.java | 74 + .../java/de/fussball/plugin/StatsManager.java | 148 ++ .../java/de/fussball/plugin/arena/Arena.java | 253 +++ .../fussball/plugin/arena/ArenaManager.java | 65 + .../plugin/commands/FussballCommand.java | 333 ++++ .../java/de/fussball/plugin/game/Ball.java | 256 +++ .../java/de/fussball/plugin/game/Game.java | 1424 +++++++++++++++++ .../de/fussball/plugin/game/GameManager.java | 133 ++ .../de/fussball/plugin/game/GameState.java | 12 + .../java/de/fussball/plugin/game/Team.java | 22 + .../plugin/listeners/BallListener.java | 222 +++ .../plugin/listeners/BlockListener.java | 22 + .../plugin/listeners/PlayerListener.java | 89 ++ .../plugin/listeners/SignListener.java | 216 +++ .../placeholders/FussballPlaceholders.java | 69 + .../plugin/scoreboard/FussballScoreboard.java | 137 ++ .../de/fussball/plugin/utils/MessageUtil.java | 31 + .../de/fussball/plugin/utils/Messages.java | 31 + src/main/resources/config.yml | 119 ++ src/main/resources/plugin.yml | 23 + 20 files changed, 3679 insertions(+) create mode 100644 src/main/java/de/fussball/plugin/Fussball.java create mode 100644 src/main/java/de/fussball/plugin/StatsManager.java create mode 100644 src/main/java/de/fussball/plugin/arena/Arena.java create mode 100644 src/main/java/de/fussball/plugin/arena/ArenaManager.java create mode 100644 src/main/java/de/fussball/plugin/commands/FussballCommand.java create mode 100644 src/main/java/de/fussball/plugin/game/Ball.java create mode 100644 src/main/java/de/fussball/plugin/game/Game.java create mode 100644 src/main/java/de/fussball/plugin/game/GameManager.java create mode 100644 src/main/java/de/fussball/plugin/game/GameState.java create mode 100644 src/main/java/de/fussball/plugin/game/Team.java create mode 100644 src/main/java/de/fussball/plugin/listeners/BallListener.java create mode 100644 src/main/java/de/fussball/plugin/listeners/BlockListener.java create mode 100644 src/main/java/de/fussball/plugin/listeners/PlayerListener.java create mode 100644 src/main/java/de/fussball/plugin/listeners/SignListener.java create mode 100644 src/main/java/de/fussball/plugin/placeholders/FussballPlaceholders.java create mode 100644 src/main/java/de/fussball/plugin/scoreboard/FussballScoreboard.java create mode 100644 src/main/java/de/fussball/plugin/utils/MessageUtil.java create mode 100644 src/main/java/de/fussball/plugin/utils/Messages.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/src/main/java/de/fussball/plugin/Fussball.java b/src/main/java/de/fussball/plugin/Fussball.java new file mode 100644 index 0000000..57be7b0 --- /dev/null +++ b/src/main/java/de/fussball/plugin/Fussball.java @@ -0,0 +1,74 @@ +package de.fussball.plugin; + +import de.fussball.plugin.arena.Arena; +import de.fussball.plugin.arena.ArenaManager; +import de.fussball.plugin.commands.FussballCommand; +import de.fussball.plugin.game.GameManager; +import de.fussball.plugin.listeners.*; +import de.fussball.plugin.placeholders.FussballPlaceholders; +import de.fussball.plugin.stats.StatsManager; +import de.fussball.plugin.utils.Messages; +import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.bukkit.plugin.java.JavaPlugin; + +public class Fussball extends JavaPlugin { + + private static Fussball instance; + private ArenaManager arenaManager; + private GameManager gameManager; + private StatsManager statsManager; + private SignListener signListener; + + @Override + public void onEnable() { + instance = this; + ConfigurationSerialization.registerClass(Arena.class); + saveDefaultConfig(); + + // Manager initialisieren + arenaManager = new ArenaManager(this); + gameManager = new GameManager(this); + statsManager = new StatsManager(this); + signListener = new SignListener(this); + Messages.init(this); + + registerCommands(); + registerListeners(); + + // PlaceholderAPI-Integration (optional – nur wenn PAPI installiert ist) + if (getServer().getPluginManager().getPlugin("PlaceholderAPI") != null) { + new FussballPlaceholders(this).register(); + getLogger().info("[Fussball] PlaceholderAPI-Integration aktiviert!"); + } else { + getLogger().info("[Fussball] PlaceholderAPI nicht gefunden – Platzhalter deaktiviert."); + } + + getLogger().info("⚽ Fußball-Plugin v" + getDescription().getVersion() + " gestartet!"); + } + + @Override + public void onDisable() { + if (gameManager != null) gameManager.stopAllGames(); + if (statsManager != null) statsManager.save(); + getLogger().info("⚽ Fußball-Plugin gestoppt!"); + } + + private void registerCommands() { + FussballCommand cmd = new FussballCommand(this); + getCommand("fussball").setExecutor(cmd); + getCommand("fussball").setTabCompleter(cmd); + } + + private void registerListeners() { + getServer().getPluginManager().registerEvents(new BallListener(this), this); + getServer().getPluginManager().registerEvents(new PlayerListener(this), this); + getServer().getPluginManager().registerEvents(new BlockListener(this), this); + getServer().getPluginManager().registerEvents(signListener, this); + } + + public static Fussball getInstance() { return instance; } + public ArenaManager getArenaManager() { return arenaManager; } + public GameManager getGameManager() { return gameManager; } + public StatsManager getStatsManager() { return statsManager; } + public SignListener getSignListener() { return signListener; } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/StatsManager.java b/src/main/java/de/fussball/plugin/StatsManager.java new file mode 100644 index 0000000..34fa4a2 --- /dev/null +++ b/src/main/java/de/fussball/plugin/StatsManager.java @@ -0,0 +1,148 @@ +package de.fussball.plugin.stats; + +import de.fussball.plugin.Fussball; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +/** + * Verwaltet persistente Spielerstatistiken in stats.yml. + * Gespeichert werden: Tore, Schüsse, Siege, Niederlagen, Unentschieden, gespielte Spiele. + */ +public class StatsManager { + + private final Fussball plugin; + private final File statsFile; + private FileConfiguration statsConfig; + + // In-Memory-Cache für schnellen Zugriff + private final Map cache = new HashMap<>(); + + public StatsManager(Fussball plugin) { + this.plugin = plugin; + this.statsFile = new File(plugin.getDataFolder(), "stats.yml"); + load(); + } + + // ── Persistenz ────────────────────────────────────────────────────────── + + private void load() { + if (!statsFile.exists()) { + try { statsFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } + } + statsConfig = YamlConfiguration.loadConfiguration(statsFile); + cache.clear(); + if (statsConfig.contains("players")) { + for (String uuidStr : statsConfig.getConfigurationSection("players").getKeys(false)) { + try { + UUID uuid = UUID.fromString(uuidStr); + 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) + ); + cache.put(uuid, stats); + } catch (IllegalArgumentException ignored) {} + } + } + plugin.getLogger().info("[Fussball] Statistiken geladen: " + cache.size() + " Spieler."); + } + + public void save() { + statsConfig.set("players", null); + for (Map.Entry 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); + } + try { statsConfig.save(statsFile); } catch (IOException e) { e.printStackTrace(); } + } + + // ── Datenzugriff ──────────────────────────────────────────────────────── + + public PlayerStats getStats(UUID uuid) { + return cache.computeIfAbsent(uuid, k -> new PlayerStats("Unbekannt", 0, 0, 0, 0, 0, 0)); + } + + public void addGoal(UUID uuid, String name) { + PlayerStats s = getStats(uuid); + s.name = name; + s.goals++; + save(); + } + + public void addKick(UUID uuid, String name) { + PlayerStats s = getStats(uuid); + s.name = name; + s.kicks++; + // Kein sofortiges Speichern bei jedem Kick – Spiel-Ende reicht + } + + public void addGameResult(UUID uuid, String name, GameResult result) { + PlayerStats s = getStats(uuid); + s.name = name; + s.games++; + switch (result) { + case WIN -> s.wins++; + case LOSS -> s.losses++; + case DRAW -> s.draws++; + } + save(); + } + + public void flushKicks(Map kicks, Map names) { + for (Map.Entry entry : kicks.entrySet()) { + PlayerStats s = getStats(entry.getKey()); + if (names.containsKey(entry.getKey())) s.name = names.get(entry.getKey()); + s.kicks += entry.getValue(); + } + save(); + } + + /** Gibt die Top-N-Torschützen zurück, sortiert nach Toren */ + public List> getTopScorers(int limit) { + List> list = new ArrayList<>(cache.entrySet()); + list.sort((a, b) -> b.getValue().goals - a.getValue().goals); + return list.subList(0, Math.min(limit, list.size())); + } + + /** Gibt die Top-N-Spieler nach Siegen zurück */ + public List> getTopWins(int limit) { + List> list = new ArrayList<>(cache.entrySet()); + list.sort((a, b) -> b.getValue().wins - a.getValue().wins); + return list.subList(0, Math.min(limit, list.size())); + } + + // ── Innere Klassen ─────────────────────────────────────────────────────── + + public static class PlayerStats { + public String name; + public int goals, kicks, wins, losses, draws, games; + + public PlayerStats(String name, int goals, int kicks, int wins, int losses, int draws, int games) { + this.name = name; this.goals = goals; this.kicks = kicks; + this.wins = wins; this.losses = losses; this.draws = draws; this.games = games; + } + + public double getWinRate() { + if (games == 0) return 0.0; + return (double) wins / games * 100.0; + } + } + + public enum GameResult { WIN, LOSS, DRAW } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/arena/Arena.java b/src/main/java/de/fussball/plugin/arena/Arena.java new file mode 100644 index 0000000..eb87fa6 --- /dev/null +++ b/src/main/java/de/fussball/plugin/arena/Arena.java @@ -0,0 +1,253 @@ +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; + 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); } + + /** + * 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)); + 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")); + 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 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; } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/arena/ArenaManager.java b/src/main/java/de/fussball/plugin/arena/ArenaManager.java new file mode 100644 index 0000000..635c932 --- /dev/null +++ b/src/main/java/de/fussball/plugin/arena/ArenaManager.java @@ -0,0 +1,65 @@ +package de.fussball.plugin.arena; + +import de.fussball.plugin.Fussball; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import java.io.*; +import java.util.*; + +public class ArenaManager { + + private final Fussball plugin; + private final Map arenas = new HashMap<>(); + private final File arenaFile; + private FileConfiguration arenaConfig; + + public ArenaManager(Fussball plugin) { + this.plugin = plugin; + this.arenaFile = new File(plugin.getDataFolder(), "arenas.yml"); + loadArenas(); + } + + public void loadArenas() { + if (!arenaFile.exists()) { + try { arenaFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } + } + arenaConfig = YamlConfiguration.loadConfiguration(arenaFile); + arenas.clear(); + if (arenaConfig.contains("arenas")) { + for (String key : arenaConfig.getConfigurationSection("arenas").getKeys(false)) { + Object obj = arenaConfig.get("arenas." + key); + if (obj instanceof Arena) arenas.put(key.toLowerCase(), (Arena) obj); + } + } + plugin.getLogger().info("Arenen geladen: " + arenas.size()); + } + + public void saveArenas() { + arenaConfig.set("arenas", null); + for (Map.Entry e : arenas.entrySet()) + arenaConfig.set("arenas." + e.getKey(), e.getValue()); + try { arenaConfig.save(arenaFile); } catch (IOException e) { e.printStackTrace(); } + } + + public Arena createArena(String name) { + Arena arena = new Arena(name); + arenas.put(name.toLowerCase(), arena); + saveArenas(); + return arena; + } + + public boolean deleteArena(String name) { + if (arenas.remove(name.toLowerCase()) != null) { saveArenas(); return true; } + return false; + } + + public Arena getArena(String name) { return arenas.get(name.toLowerCase()); } + public boolean arenaExists(String name) { return arenas.containsKey(name.toLowerCase()); } + public Collection getAllArenas() { return arenas.values(); } + public List getArenaNames() { return new ArrayList<>(arenas.keySet()); } + + public void saveArena(Arena arena) { + arenas.put(arena.getName().toLowerCase(), arena); + saveArenas(); + } +} diff --git a/src/main/java/de/fussball/plugin/commands/FussballCommand.java b/src/main/java/de/fussball/plugin/commands/FussballCommand.java new file mode 100644 index 0000000..ebb1938 --- /dev/null +++ b/src/main/java/de/fussball/plugin/commands/FussballCommand.java @@ -0,0 +1,333 @@ +package de.fussball.plugin.commands; + +import de.fussball.plugin.Fussball; +import de.fussball.plugin.arena.Arena; +import de.fussball.plugin.game.Ball; +import de.fussball.plugin.game.Game; +import de.fussball.plugin.game.GameState; +import de.fussball.plugin.stats.StatsManager; +import de.fussball.plugin.utils.MessageUtil; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.command.*; +import org.bukkit.entity.Player; + +import java.util.*; + +public class FussballCommand implements CommandExecutor, TabCompleter { + + private final Fussball plugin; + public FussballCommand(Fussball plugin) { this.plugin = plugin; } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 0) { sendHelp(sender); return true; } + + switch (args[0].toLowerCase()) { + + // ── Spieler-Befehle ────────────────────────────────────────────── + + case "join" -> { + if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; } + if (args.length < 2) { player.sendMessage(MessageUtil.error("Benutze: /fb join ")); return true; } + Arena arena = plugin.getArenaManager().getArena(args[1]); + if (arena == null) { player.sendMessage(MessageUtil.error("Arena §e" + args[1] + " §cnicht gefunden!")); return true; } + if (!arena.isSetupComplete()) { player.sendMessage(MessageUtil.error("Arena nicht vollständig eingerichtet!")); return true; } + if (plugin.getGameManager().isInAnyGame(player)) { player.sendMessage(MessageUtil.error("Du bist bereits in einem Spiel! Tippe §e/fb leave§c.")); return true; } + plugin.getGameManager().createGame(arena).addPlayer(player); + } + + case "leave" -> { + if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; } + // Aus Spiel + Game game = plugin.getGameManager().getPlayerGame(player); + if (game != null) { game.removePlayer(player); player.sendMessage(MessageUtil.success("Du hast das Spiel verlassen!")); return true; } + // Aus Zuschauer + Game specGame = plugin.getGameManager().getSpectatorGame(player); + if (specGame != null) { specGame.removeSpectator(player); player.sendMessage(MessageUtil.success("Du hast das Zuschauen beendet!")); return true; } + // Aus Warteschlange + plugin.getGameManager().removeFromAllQueues(player); + player.sendMessage(MessageUtil.warn("Du bist in keinem Spiel und in keiner Warteschlange.")); + } + + case "spectate", "spec" -> { + if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; } + if (args.length < 2) { player.sendMessage(MessageUtil.error("Benutze: /fb spectate ")); return true; } + Arena arena = plugin.getArenaManager().getArena(args[1]); + if (arena == null) { player.sendMessage(MessageUtil.error("Arena nicht gefunden!")); return true; } + if (plugin.getGameManager().isInAnyGame(player)) { player.sendMessage(MessageUtil.error("Verlasse zuerst dein aktuelles Spiel!")); return true; } + Game game = plugin.getGameManager().getGame(arena.getName()); + if (game == null) { player.sendMessage(MessageUtil.error("Kein laufendes Spiel in dieser Arena!")); return true; } + game.addSpectator(player); + } + + case "list" -> { + sender.sendMessage(MessageUtil.header("⚽ Verfügbare Arenen")); + Collection arenas = plugin.getArenaManager().getAllArenas(); + if (arenas.isEmpty()) { sender.sendMessage(MessageUtil.warn("Keine Arenen vorhanden.")); return true; } + for (Arena a : arenas) { + Game g = plugin.getGameManager().getGame(a.getName()); + int queueSize = plugin.getGameManager().getQueueSize(a.getName()); + String status = g != null ? statusDot(g.getState()) : "§a●"; + String players = g != null ? g.getAllPlayers().size() + "/" + a.getMaxPlayers() : "0/" + a.getMaxPlayers(); + String setup = a.isSetupComplete() ? "§a✔" : "§c✗"; + String queue = queueSize > 0 ? " §8(§e" + queueSize + " §8warten)" : ""; + sender.sendMessage("§7 " + status + " §e" + a.getName() + " §7[" + players + "] " + setup + queue); + } + } + + case "stats" -> { + if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; } + Player target = args.length >= 2 ? Bukkit.getPlayer(args[1]) : player; + if (target == null) { player.sendMessage(MessageUtil.error("Spieler §e" + args[1] + " §cnicht gefunden!")); return true; } + 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 👟 Schüsse: §e" + s.kicks); + player.sendMessage("§7 🏆 Siege: §a" + s.wins); + player.sendMessage("§7 ❌ Niederlagen: §c" + s.losses); + player.sendMessage("§7 ➖ Unentschieden: §7" + s.draws); + player.sendMessage("§7 📊 Gespielte Spiele: §e" + s.games); + player.sendMessage("§7 📈 Siegquote: §e" + String.format("%.1f", s.getWinRate()) + "§7%"); + // In-Game-Statistik anhängen wenn aktiv + Game inGame = plugin.getGameManager().getPlayerGame(target); + if (inGame != null) { + player.sendMessage("§8--- Aktuelles Spiel ---"); + player.sendMessage("§7 Tore heute: §e" + inGame.getGoals().getOrDefault(target.getUniqueId(), 0)); + player.sendMessage("§7 Schüsse heute: §e" + inGame.getKicks().getOrDefault(target.getUniqueId(), 0)); + } + } + + // ── Admin-Befehle ──────────────────────────────────────────────── + + case "create" -> { + if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; } + if (args.length < 2) { sender.sendMessage(MessageUtil.error("Benutze: /fb create ")); return true; } + if (plugin.getArenaManager().arenaExists(args[1])) { sender.sendMessage(MessageUtil.error("Arena §e" + args[1] + " §cexistiert bereits!")); return true; } + plugin.getArenaManager().createArena(args[1]); + sender.sendMessage(MessageUtil.success("Arena §e" + args[1] + " §aerstellt! Richte sie mit §e/fb setup §aein.")); + } + + case "delete" -> { + if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; } + if (args.length < 2) { sender.sendMessage(MessageUtil.error("Benutze: /fb delete ")); return true; } + sender.sendMessage(plugin.getArenaManager().deleteArena(args[1]) + ? MessageUtil.success("Arena §e" + args[1] + " §agelöscht!") : MessageUtil.error("Arena nicht gefunden!")); + } + + case "setup" -> { + if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; } + if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; } + if (args.length < 3) { sendSetupHelp(player); return true; } + handleSetup(player, args); + } + + case "stop" -> { + if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; } + if (args.length < 2) { sender.sendMessage(MessageUtil.error("Benutze: /fb stop ")); return true; } + Game game = plugin.getGameManager().getGame(args[1]); + if (game == null) { sender.sendMessage(MessageUtil.error("Kein aktives Spiel in §e" + args[1] + "§c!")); return true; } + game.endGame(null); + sender.sendMessage(MessageUtil.success("Spiel in §e" + args[1] + " §aberendet.")); + } + + case "top" -> { + if (args.length < 2) { + sender.sendMessage(MessageUtil.error("Benutze: /fussball top goals|wins|kicks")); + return true; + } + switch (args[1].toLowerCase()) { + case "goals" -> { + sender.sendMessage(MessageUtil.header("🏆 Top Torschützen")); + var list = plugin.getStatsManager().getTopScorers(10); + if (list.isEmpty()) { sender.sendMessage(MessageUtil.warn("Noch keine Daten.")); break; } + for (int i = 0; i < list.size(); i++) { + var e = list.get(i); + sender.sendMessage("§e#" + (i+1) + " §f" + e.getValue().name + + " §7— §e" + e.getValue().goals + " §7Tore" + + " §8(§7" + e.getValue().games + " Spiele§8)"); + } + } + case "wins" -> { + sender.sendMessage(MessageUtil.header("🏆 Top Gewinner")); + var list = plugin.getStatsManager().getTopWins(10); + if (list.isEmpty()) { sender.sendMessage(MessageUtil.warn("Noch keine Daten.")); break; } + for (int i = 0; i < list.size(); i++) { + var e = list.get(i); + sender.sendMessage("§e#" + (i+1) + " §f" + e.getValue().name + + " §7— §a" + e.getValue().wins + " §7Siege" + + " §8(§7" + String.format("%.0f", e.getValue().getWinRate()) + "% WR§8)"); + } + } + case "kicks" -> { + sender.sendMessage(MessageUtil.header("🏆 Top Schützen (Schüsse)")); + var list = plugin.getStatsManager().getTopScorers(10); + // Nutze getTopScorers und zeige kicks + var kickList = new java.util.ArrayList<>(plugin.getStatsManager().getTopScorers(100)); + kickList.sort((a, b) -> b.getValue().kicks - a.getValue().kicks); + for (int i = 0; i < Math.min(10, kickList.size()); i++) { + var e = kickList.get(i); + sender.sendMessage("§e#" + (i+1) + " §f" + e.getValue().name + + " §7— §e" + e.getValue().kicks + " §7Schüsse"); + } + } + default -> sender.sendMessage(MessageUtil.error("Gültig: goals | wins | kicks")); + } + } + + case "debug" -> { + if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; } + if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; } + if (args.length < 2) { sender.sendMessage(MessageUtil.error("Benutze: /fb debug ")); return true; } + Arena arena = plugin.getArenaManager().getArena(args[1]); + if (arena == null) { sender.sendMessage(MessageUtil.error("Arena nicht gefunden!")); return true; } + handleDebug(player, arena); + } + + default -> sendHelp(sender); + } + return true; + } + + // ── Setup-Handler ──────────────────────────────────────────────────────── + + private void handleSetup(Player player, String[] args) { + Arena arena = plugin.getArenaManager().getArena(args[1]); + if (arena == null) { player.sendMessage(MessageUtil.error("Arena §e" + args[1] + " §cnicht gefunden!")); return; } + + switch (args[2].toLowerCase()) { + case "lobby" -> { arena.setLobby(player.getLocation()); player.sendMessage(MessageUtil.success("Lobby gesetzt: " + locStr(player.getLocation()))); } + case "redspawn" -> { arena.setRedSpawn(player.getLocation()); player.sendMessage(MessageUtil.success("Roter Spawn gesetzt: " + locStr(player.getLocation()))); } + case "bluespawn" -> { arena.setBlueSpawn(player.getLocation()); player.sendMessage(MessageUtil.success("Blauer Spawn gesetzt: " + locStr(player.getLocation()))); } + case "ballspawn" -> { arena.setBallSpawn(player.getLocation()); player.sendMessage(MessageUtil.success("Ball-Spawn gesetzt: " + locStr(player.getLocation()))); } + case "center" -> { arena.setCenter(player.getLocation()); player.sendMessage(MessageUtil.success("Mittelpunkt gesetzt: " + locStr(player.getLocation()))); } + case "redgoalmin" -> { arena.setRedGoalMin(player.getLocation()); player.sendMessage(MessageUtil.success("Rotes Tor Min gesetzt: " + locStr(player.getLocation()))); } + 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 "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")); } + case "info" -> { + player.sendMessage(MessageUtil.header("Arena: " + arena.getName())); + player.sendMessage("§7 Lobby: " + check(arena.getLobby())); + player.sendMessage("§7 Roter Spawn: " + check(arena.getRedSpawn())); + player.sendMessage("§7 Blauer Spawn: " + check(arena.getBlueSpawn())); + player.sendMessage("§7 Ball-Spawn: " + check(arena.getBallSpawn())); + player.sendMessage("§7 Mittelpunkt: " + check(arena.getCenter())); + 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 Min. Spieler: §e" + arena.getMinPlayers()); + player.sendMessage("§7 Max. Spieler: §e" + arena.getMaxPlayers()); + player.sendMessage("§7 Spieldauer: §e" + arena.getGameDuration() + "s"); + player.sendMessage("§7 Setup komplett: " + (arena.isSetupComplete() ? "§a✔ JA" : "§c✗ NEIN")); + return; + } + default -> { sendSetupHelp(player); return; } + } + plugin.getArenaManager().saveArena(arena); + } + + // ── Debug-Handler ──────────────────────────────────────────────────────── + + private void handleDebug(Player player, Arena arena) { + player.sendMessage(MessageUtil.header("DEBUG: " + arena.getName())); + printRegion(player, "§cROTES TOR", arena.getRedGoalMin(), arena.getRedGoalMax()); + printRegion(player, "§9BLAUES TOR", arena.getBlueGoalMin(), arena.getBlueGoalMax()); + printRegion(player, "§aSPIELFELD", arena.getFieldMin(), arena.getFieldMax()); + + Game game = plugin.getGameManager().getGame(arena.getName()); + if (game != null && game.getBall() != null && game.getBall().isActive()) { + Ball ball = game.getBall(); + Location bl = ball.getEntity().getLocation(); + Location bh = bl.clone().add(0, 1.4, 0); + player.sendMessage("§e--- BALL ---"); + player.sendMessage("§7Fuß: §f" + locStr(bl)); + player.sendMessage("§7Kopf: §f" + locStr(bh)); + player.sendMessage("§7RotesTor: Fuß=" + yn(arena.isInRedGoal(bl)) + " Kopf=" + yn(arena.isInRedGoal(bh))); + player.sendMessage("§7BlauesTor: Fuß=" + yn(arena.isInBlueGoal(bl)) + " Kopf=" + yn(arena.isInBlueGoal(bh))); + player.sendMessage("§7Im Feld: " + yn(arena.isInField(bl))); + player.sendMessage("§7Aus-Seite: §f" + (arena.getOutSide(bl) != null ? arena.getOutSide(bl) : "keine")); + } else { + player.sendMessage("§7(Kein aktiver Ball)"); + } + player.sendMessage("§e--- DEINE POSITION ---"); + player.sendMessage("§f" + locStr(player.getLocation())); + player.sendMessage("§7RotesTor: " + yn(arena.isInRedGoal(player.getLocation()))); + player.sendMessage("§7BlauesTor: " + yn(arena.isInBlueGoal(player.getLocation()))); + player.sendMessage("§7Im Feld: " + yn(arena.isInField(player.getLocation()))); + } + + // ── Hilfsmethoden ──────────────────────────────────────────────────────── + + private String statusDot(GameState s) { + return switch (s) { + case WAITING -> "§a●"; + case STARTING -> "§e●"; + case RUNNING, GOAL, HALFTIME, OVERTIME -> "§c●"; + case PENALTY -> "§d●"; + case ENDING -> "§7●"; + }; + } + + private void printRegion(Player p, String label, Location min, Location max) { + p.sendMessage("§e--- " + label + " §e---"); + if (min == null || max == null) { p.sendMessage("§cNICHT GESETZT"); return; } + p.sendMessage("§7Min: §f" + locStr(min) + " §7Max: §f" + locStr(max)); + p.sendMessage("§7X: §f" + fmt(Math.min(min.getX(), max.getX())) + "§7─§f" + fmt(Math.max(min.getX(), max.getX()))); + p.sendMessage("§7Y: §f" + fmt(Math.min(min.getY(), max.getY())) + "§7─§f" + fmt(Math.max(min.getY(), max.getY()))); + p.sendMessage("§7Z: §f" + fmt(Math.min(min.getZ(), max.getZ())) + "§7─§f" + fmt(Math.max(min.getZ(), max.getZ()))); + } + + private void sendHelp(CommandSender s) { + s.sendMessage(MessageUtil.header("⚽ Fußball Plugin")); + s.sendMessage("§e/fb join §7- Spiel beitreten"); + s.sendMessage("§e/fb leave §7- Spiel / Zuschauer verlassen"); + s.sendMessage("§e/fb spectate §7- Spiel zuschauen"); + s.sendMessage("§e/fb list §7- Arenen anzeigen"); + s.sendMessage("§e/fb stats [spieler] §7- Statistiken anzeigen"); + s.sendMessage("§e/fb top [goals|wins] §7- Bestenliste"); + if (s.hasPermission("fussball.admin")) { + s.sendMessage("§c§lAdmin: §ccreate / delete / setup / stop / debug"); + } + } + + private void sendSetupHelp(Player p) { + p.sendMessage(MessageUtil.header("Setup-Optionen")); + p.sendMessage("§e/fb setup lobby|redspawn|bluespawn|ballspawn|center"); + p.sendMessage("§e/fb setup redgoalmin|redgoalmax|bluegoalmin|bluegoalmax"); + p.sendMessage("§e/fb setup fieldmin|fieldmax §8(optional – Aus-Erkennung)"); + p.sendMessage("§e/fb setup minplayers |maxplayers |duration |info"); + } + + private String check(Location l) { return l != null ? "§a✔ " + locStr(l) : "§c✗ nicht gesetzt"; } + private String check(Location a, Location b) { return (a != null && b != null) ? "§a✔ gesetzt" : "§c✗ nicht gesetzt"; } + private String yn(boolean b) { return b ? "§aJA" : "§cNEIN"; } + private String locStr(Location l) { return fmt(l.getX()) + " / " + fmt(l.getY()) + " / " + fmt(l.getZ()); } + private String fmt(double d) { return String.format("%.1f", d); } + + // ── Tab-Completion ─────────────────────────────────────────────────────── + + @Override + public List onTabComplete(CommandSender sender, Command cmd, String alias, String[] args) { + List list = new ArrayList<>(); + if (args.length == 1) { + list.addAll(List.of("join", "leave", "list", "stats", "top", "spectate")); + if (sender.hasPermission("fussball.admin")) list.addAll(List.of("create", "delete", "setup", "stop", "debug")); + } else if (args.length == 2 && List.of("join","delete","setup","stop","debug","spectate").contains(args[0].toLowerCase())) { + list.addAll(plugin.getArenaManager().getArenaNames()); + } 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")); + } else if (args.length == 2 && args[0].equalsIgnoreCase("top")) { + list.addAll(List.of("goals", "wins")); + } + String input = args[args.length - 1].toLowerCase(); + list.removeIf(s -> !s.toLowerCase().startsWith(input)); + return list; + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/game/Ball.java b/src/main/java/de/fussball/plugin/game/Ball.java new file mode 100644 index 0000000..c6ca3ee --- /dev/null +++ b/src/main/java/de/fussball/plugin/game/Ball.java @@ -0,0 +1,256 @@ +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; +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.Vector; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.UUID; + +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; + + private final Game game; + private final Fussball plugin; + private ArmorStand entity; + private final Location spawnLocation; + private boolean active = false; + + // ── Torwart-Halten ─────────────────────────────────────────────────────── + private boolean heldByGoalkeeper = false; + private Player holdingPlayer = null; + + public Ball(Game game, Fussball plugin, Location spawnLocation) { + this.game = game; + this.plugin = plugin; + this.spawnLocation = spawnLocation.clone(); + } + + // ── Config-Helfer ──────────────────────────────────────────────────────── + + private double cfg(String path, double def) { + return plugin.getConfig().getDouble(path, def); + } + + // ── 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( + spawnLocation, ArmorStand.class, CreatureSpawnEvent.SpawnReason.CUSTOM, false, + stand -> { + stand.setVisible(false); + stand.setGravity(true); + stand.setCollidable(false); + stand.setInvulnerable(true); + stand.setPersistent(false); + stand.setSilent(true); + stand.setSmall(true); + stand.setArms(false); + stand.setBasePlate(false); + stand.setCustomName("§e⚽ Fußball"); + stand.setCustomNameVisible(true); + stand.getEquipment().setHelmet(createBallItem()); + } + ); + + if (entity == null) { + plugin.getLogger().severe("[Fussball] ArmorStand konnte nicht gespawnt werden!"); return; + } + active = true; + } + + private ItemStack createBallItem() { + ItemStack skull = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) skull.getItemMeta(); + if (meta == null) return skull; + meta.setDisplayName("§e⚽ Fußball"); + try { + PlayerProfile profile = Bukkit.createPlayerProfile( + UUID.nameUUIDFromBytes("FussballBall".getBytes()), "FussballBall"); + PlayerTextures textures = profile.getTextures(); + textures.setSkin(new URL(BALL_TEXTURE_URL)); + profile.setTextures(textures); + meta.setOwnerProfile(profile); + } catch (MalformedURLException e) { + plugin.getLogger().warning("[Fussball] Ball-Textur URL ungültig: " + e.getMessage()); + } + skull.setItemMeta(meta); + return skull; + } + + // ── Schuss ─────────────────────────────────────────────────────────────── + + /** Normaler Schuss (Rechtsklick) */ + public void kick(Player player) { + if (entity == null || entity.isDead() || !active) return; + Location ballLoc = entity.getLocation(); + Vector dir = getKickDirection(player); + + 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); + applyKick(dir, ballLoc, 1.8f); + } + + /** + * Aufgeladener Schuss (Shift gedrückt halten → loslassen) + * power = 0.0 (kurz gehalten) bis 1.0 (voll aufgeladen, ~1.5s) + */ + public void chargedKick(Player player, double power) { + if (entity == null || entity.isDead() || !active) return; + Location ballLoc = entity.getLocation(); + Vector dir = getKickDirection(player); + + double minPower = cfg("ball.charged-min-power", 1.3); + double maxPower = cfg("ball.charged-max-power", 3.8); + double actualMultiplier = 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); + 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); + } + } + + 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; + } + + private void applyKick(Vector dir, Location ballLoc, float soundPitch) { + entity.setVelocity(dir); + 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 ───────────────────────────────────────────────────── + + /** + * Torwart greift den Ball – Ball schwebt vor ihm und folgt ihm. + * Prüft vorher, ob er im erlaubten Bereich ist. + */ + public void holdBall(Player goalkeeper) { + 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; + } + + this.heldByGoalkeeper = true; + this.holdingPlayer = goalkeeper; + entity.setGravity(false); + entity.setVelocity(new Vector(0, 0, 0)); + } + + /** + * 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(); + hold.add(holdingPlayer.getLocation().getDirection().normalize().multiply(0.8)); + hold.setY(hold.getY() + 1.0); + entity.teleport(hold); + entity.setVelocity(new Vector(0, 0, 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); + } + } + + /** + * 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); + entity.setVelocity(dir); + 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 ───────────────────────────────────────────────────────────────── + + public void returnToCenter() { + if (entity != null && !entity.isDead()) { + entity.teleport(spawnLocation); + entity.setVelocity(new Vector(0, 0, 0)); + } + } + + public void remove() { + if (entity != null && !entity.isDead()) entity.remove(); + entity = null; + active = false; + } + + public ArmorStand getEntity() { return entity; } + public boolean isActive() { return active; } + public Location getSpawnLocation() { return spawnLocation; } + + public double getDistanceTo(Player player) { + if (entity == null || entity.isDead()) return Double.MAX_VALUE; + if (!entity.getWorld().equals(player.getWorld())) return Double.MAX_VALUE; + return entity.getLocation().distance(player.getLocation()); + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/game/Game.java b/src/main/java/de/fussball/plugin/game/Game.java new file mode 100644 index 0000000..0eb4455 --- /dev/null +++ b/src/main/java/de/fussball/plugin/game/Game.java @@ -0,0 +1,1424 @@ +package de.fussball.plugin.game; + +import de.fussball.plugin.Fussball; +import de.fussball.plugin.arena.Arena; +import de.fussball.plugin.scoreboard.FussballScoreboard; +import de.fussball.plugin.stats.StatsManager; +import de.fussball.plugin.utils.MessageUtil; +import de.fussball.plugin.utils.Messages; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.*; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; +import org.bukkit.entity.Firework; +import org.bukkit.entity.Player; +import org.bukkit.FireworkEffect; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.FireworkMeta; +import org.bukkit.potion.PotionEffect; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.Vector; + +import java.util.*; +import java.util.stream.Collectors; + +public class Game { + + private final Fussball plugin; + private final Arena arena; + + // ── Spielzustand ──────────────────────────────────────────────────────── + private GameState state = GameState.WAITING; + + // ── Spielerlisten ─────────────────────────────────────────────────────── + private final List redTeam = new ArrayList<>(); + private final List blueTeam = new ArrayList<>(); + private final List allPlayers = new ArrayList<>(); + private final List spectators = new ArrayList<>(); + + // ── Spielstand / Zeit ─────────────────────────────────────────────────── + private int redScore = 0, blueScore = 0, timeLeft; + private boolean secondHalf = false; // true = 2. Halbzeit / Verlängerung + private boolean overtimeDone = false; // Verlängerung bereits gespielt? + + // ── Statistik ────────────────────────────────────────────────────────── + private final Map goals = new HashMap<>(); + private final Map kicks = new HashMap<>(); + + // ── FEHLENDE VARIABLE ──────────────────────────────────────────────── + private final Map outOfBoundsCountdown = new HashMap<>(); + // ──────────────────────────────────────────────────────────────────────── + + private UUID lastKicker = null; + private Team lastTouchTeam = null; + private Team throwInTeam = null; // null = alle dürfen; gesetzt = nur dieses Team darf treten + + // ── Ball ─────────────────────────────────────────────────────────────── + private Ball ball; + private Location lastBallLocation = null; + private boolean outCooldown = false; + private int spawnCooldown = 0; + private int ballMissingTicks = 0; // Ticks ohne lebenden Ball → Respawn + private static final int BALL_MISSING_TIMEOUT = 80; // 4s + + // ── Torwart ──────────────────────────────────────────────────────────── + private UUID redGoalkeeper = null; + private UUID blueGoalkeeper = null; + + // ── Karten ──────────────────────────────────────────────────────────── + private final Map yellowCards = new HashMap<>(); + private final Set redCards = new HashSet<>(); + + // ── Freistoß ────────────────────────────────────────────────────────── + private Location freekickLocation = null; // Wo der Ball liegt beim Freistoß + private int freekickTicks = 0; // Countdown bis Freistoß automatisch freigegeben + private boolean offsideCooldown = false;// Verhindert sofortigen erneuten Abseits + + // ── Matchbericht ────────────────────────────────────────────────────── + private final List matchEvents = new ArrayList<>(); + private int secondsPlayed = 0; // Für Minutenangaben im Bericht + + // ── Tasks ────────────────────────────────────────────────────────────── + private BukkitTask gameTask, countdownTask, goalCheckTask; + + // ── Scoreboard / BossBar ─────────────────────────────────────────────── + private final FussballScoreboard scoreboard; + private BossBar bossBar; + + // ── Elfmeter ─────────────────────────────────────────────────────────── + private int penaltyRound = 0; + private int penaltyMaxRounds = 5; + private int penaltyRedGoals = 0; + private int penaltyBlueGoals = 0; + private int penaltyRedShots = 0; + private int penaltyBlueShots = 0; + private Team penaltyTurn = Team.RED; // Wer schießt gerade? + private int penaltyShooterIndex = 0; // Index im Team-Array + private BukkitTask penaltyShotTask = null; + private boolean penaltyGoalScored = false; + + public Game(Fussball plugin, Arena arena) { + this.plugin = plugin; + this.arena = arena; + this.timeLeft = arena.getGameDuration(); + this.scoreboard = new FussballScoreboard(this); + } + + // ════════════════════════════════════════════════════════════════════════ + // SPIELER-VERWALTUNG + // ════════════════════════════════════════════════════════════════════════ + + public boolean addPlayer(Player player) { + if (allPlayers.size() >= arena.getMaxPlayers()) { + // Arena voll → Warteschlange + plugin.getGameManager().addToQueue(arena.getName(), player); + return false; + } + if (isInGame(player)) { player.sendMessage(MessageUtil.error("Du bist bereits im Spiel!")); return false; } + if (isSpectator(player)) removeSpectator(player); + if (state != GameState.WAITING && state != GameState.STARTING) { + player.sendMessage(MessageUtil.error("Das Spiel läuft bereits! Tippe §e/fb spectate " + arena.getName() + " §czum Zuschauen.")); return false; + } + + allPlayers.add(player.getUniqueId()); + assignTeam(player); + player.teleport(arena.getLobby()); + preparePlayer(player); + broadcastAll(MessageUtil.info("§e" + player.getName() + " §7ist beigetreten! §8(" + allPlayers.size() + "/" + arena.getMaxPlayers() + ")")); + if (allPlayers.size() >= arena.getMinPlayers() && state == GameState.WAITING) startCountdown(); + scoreboard.updateAll(); + refreshSigns(); + return true; + } + + public void removePlayer(Player player) { + UUID uuid = player.getUniqueId(); + if (!isInGame(player)) return; + allPlayers.remove(uuid); redTeam.remove(uuid); blueTeam.remove(uuid); + if (bossBar != null) bossBar.removePlayer(player); + scoreboard.remove(player); + resetPlayer(player); + broadcastAll(MessageUtil.info("§e" + player.getName() + " §7hat das Spiel verlassen!")); + if ((state == GameState.RUNNING || state == GameState.OVERTIME) && allPlayers.size() < arena.getMinPlayers()) endGame(null); + scoreboard.updateAll(); + refreshSigns(); + } + + // ── Zuschauer ─────────────────────────────────────────────────────────── + + public boolean addSpectator(Player player) { + if (isInGame(player)) { player.sendMessage(MessageUtil.error("Du bist bereits Spieler!")); return false; } + if (isSpectator(player)) { player.sendMessage(MessageUtil.error("Du schaust bereits zu!")); return false; } + if (state == GameState.WAITING || state == GameState.STARTING || state == GameState.ENDING) { + player.sendMessage(MessageUtil.error("Kein laufendes Spiel zum Zuschauen!")); return false; + } + spectators.add(player.getUniqueId()); + player.setGameMode(GameMode.SPECTATOR); + player.teleport(arena.getBallSpawn() != null ? arena.getBallSpawn() : arena.getLobby()); + scoreboard.give(player); + if (bossBar != null) bossBar.addPlayer(player); + player.sendMessage(MessageUtil.success("Du schaust jetzt §e" + arena.getName() + " §azu! §7(/fb leave zum Beenden)")); + return true; + } + + public void removeSpectator(Player player) { + spectators.remove(player.getUniqueId()); + if (bossBar != null) bossBar.removePlayer(player); + scoreboard.remove(player); + resetPlayer(player); + } + + public boolean isSpectator(Player player) { return spectators.contains(player.getUniqueId()); } + public List getSpectators() { return spectators; } + + // ── Team-Zuweisung ─────────────────────────────────────────────────────── + + /** Auto-Balance: immer ins kleinere Team */ + private void assignTeam(Player player) { + if (redTeam.size() <= blueTeam.size()) { + redTeam.add(player.getUniqueId()); + player.sendMessage(MessageUtil.success("Du bist im §cRoten Team§a!")); + } else { + blueTeam.add(player.getUniqueId()); + player.sendMessage(MessageUtil.success("Du bist im §9Blauen Team§a!")); + } + } + + // ── Spieler vorbereiten / zurücksetzen ─────────────────────────────────── + + private void preparePlayer(Player player) { + player.getInventory().clear(); + player.setHealth(20); player.setFoodLevel(20); player.setSaturation(20f); + player.setExp(0); player.setLevel(0); + for (PotionEffect e : player.getActivePotionEffects()) player.removePotionEffect(e.getType()); + player.setGameMode(GameMode.ADVENTURE); + Team team = getTeam(player); + if (team != null) applyTeamColors(player, team); + scoreboard.give(player); + if (bossBar != null) bossBar.addPlayer(player); + } + + private void applyTeamColors(Player player, Team team) { + org.bukkit.Color color = team == Team.RED ? org.bukkit.Color.RED : org.bukkit.Color.BLUE; + ItemStack[] armor = { + new ItemStack(Material.LEATHER_HELMET), new ItemStack(Material.LEATHER_CHESTPLATE), + new ItemStack(Material.LEATHER_LEGGINGS), new ItemStack(Material.LEATHER_BOOTS) + }; + for (ItemStack item : armor) { + if (item.getItemMeta() instanceof org.bukkit.inventory.meta.LeatherArmorMeta meta) { + meta.setColor(color); item.setItemMeta(meta); + } + } + player.getInventory().setHelmet(armor[0]); player.getInventory().setChestplate(armor[1]); + player.getInventory().setLeggings(armor[2]); player.getInventory().setBoots(armor[3]); + } + + /** BUG FIX: Teleportiert zur Lobby statt Welt-Spawn */ + private void resetPlayer(Player player) { + player.getInventory().clear(); + for (PotionEffect e : player.getActivePotionEffects()) player.removePotionEffect(e.getType()); + player.setGameMode(GameMode.SURVIVAL); + Location tp = arena.getLobby() != null ? arena.getLobby() : Bukkit.getWorlds().get(0).getSpawnLocation(); + player.teleport(tp); + } + + // ════════════════════════════════════════════════════════════════════════ + // COUNTDOWN + // ════════════════════════════════════════════════════════════════════════ + + private void startCountdown() { + state = GameState.STARTING; + refreshSigns(); + final int[] cd = {10}; + countdownTask = new BukkitRunnable() { + public void run() { + if (allPlayers.size() < arena.getMinPlayers()) { + broadcastAll(MessageUtil.warn("Zu wenig Spieler! Countdown abgebrochen.")); + state = GameState.WAITING; refreshSigns(); cancel(); return; + } + if (cd[0] == 0) { startGame(); cancel(); return; } + if (cd[0] <= 5 || cd[0] % 5 == 0) { + broadcastAll("§e⚽ §6Spiel startet in §e" + cd[0] + " §6Sekunden!"); + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1f); p.sendTitle("§e" + cd[0], "§7Bereite dich vor!", 5, 15, 5); } + } + } + cd[0]--; + } + }.runTaskTimer(plugin, 0L, 20L); + } + + // ════════════════════════════════════════════════════════════════════════ + // SPIELSTART + // ════════════════════════════════════════════════════════════════════════ + + private void startGame() { + state = GameState.RUNNING; + secondHalf = false; + timeLeft = arena.getGameDuration(); + secondsPlayed = 0; + + bossBar = Bukkit.createBossBar("§e⚽ §c0 §7: §90 §8| §e05:00", BarColor.GREEN, BarStyle.SOLID); + bossBar.setProgress(1.0); + + for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getRedSpawn()); preparePlayer(p); } } + for (UUID uuid : blueTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getBlueSpawn()); preparePlayer(p); } } + + // Torwart auto-zuweisen (erster Spieler jedes Teams) + assignGoalkeepers(); + + broadcastAll("§a§l⚽ DAS SPIEL BEGINNT! ⚽"); + broadcastAll("§c▶ Rotes Team §7vs §9Blaues Team ◀"); + + // 5-Sekunden-Countdown vor Anstoß – Spieler stehen bereits an ihren Positionen + new BukkitRunnable() { + int cd = 5; + public void run() { + if (cd > 0) { + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { + p.sendTitle("§e" + cd, "§7Nimm deine Position ein!", 0, 25, 5); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1f); + } + } + cd--; + } else { + spawnBallDelayed(arena.getBallSpawn()); + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { + p.sendTitle("§a§lANPFIFF!", "§7Viel Erfolg!", 10, 40, 10); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 2f, 1.5f); + } + } + cancel(); + } + } + }.runTaskTimer(plugin, 0L, 20L); + + refreshSigns(); + startGameLoop(); + startGoalCheckLoop(); + } + + // ════════════════════════════════════════════════════════════════════════ + // HALBZEIT + // ════════════════════════════════════════════════════════════════════════ + + private void startHalfTime() { + state = GameState.HALFTIME; + if (ball != null) { ball.remove(); ball = null; } + + broadcastAll("§e§l╔═══════════════════╗"); + broadcastAll("§e§l║ ⏸ HALBZEIT! ║"); + broadcastAll("§e§l╚═══════════════════╝"); + broadcastAll("§7Spielstand: §c" + redScore + " §7: §9" + blueScore); + broadcastAll("§7In §e30 Sekunden §7geht es weiter!"); + + if (bossBar != null) { bossBar.setTitle("§e⏸ HALBZEIT §c" + redScore + " §7: §9" + blueScore); bossBar.setColor(BarColor.YELLOW); } + + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { p.sendTitle("§e§lHALBZEIT!", "§7" + redScore + " : " + blueScore, 10, 60, 10); p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 0.8f); } + } + scoreboard.updateAll(); + + new BukkitRunnable() { + final int[] cd = {30}; + public void run() { + if (state != GameState.HALFTIME) { cancel(); return; } + if (cd[0] == 10 || cd[0] == 5) broadcastAll("§e⏱ 2. Halbzeit in §e" + cd[0] + " §6Sek!"); + if (cd[0] <= 0) { startSecondHalf(); cancel(); return; } + cd[0]--; + } + }.runTaskTimer(plugin, 20L, 20L); + } + + private void startSecondHalf() { + state = GameState.RUNNING; + secondHalf = true; + timeLeft = arena.getGameDuration() / 2; + + // 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 : blueTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getRedSpawn()); } + + broadcastAll("§a§l▶ 2. HALBZEIT! ◀"); + broadcastAll("§7Die Seiten wurden getauscht!"); + if (bossBar != null) bossBar.setColor(BarColor.GREEN); + scoreboard.updateAll(); + + // 5-Sekunden-Countdown vor Anstoß + new BukkitRunnable() { + int cd = 5; + public void run() { + if (cd > 0) { + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { + p.sendTitle("§e" + cd, "§7Seiten getauscht – nimm Position ein!", 0, 25, 5); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1f); + } + } + cd--; + } else { + spawnBallDelayed(arena.getBallSpawn()); + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { + p.sendTitle("§a§l2. HALBZEIT!", "§7Los geht's!", 10, 40, 10); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 2f, 1.5f); + } + } + cancel(); + } + } + }.runTaskTimer(plugin, 0L, 20L); + } + + // ════════════════════════════════════════════════════════════════════════ + // VERLÄNGERUNG + // ════════════════════════════════════════════════════════════════════════ + + private void startOvertime() { + state = GameState.OVERTIME; + overtimeDone = true; + timeLeft = 600; // 2x5min = 10 Min Verlängerung + + if (ball != null) ball.remove(); + spawnBallDelayed(arena.getBallSpawn()); + + broadcastAll("§6§l╔══════════════════════╗"); + broadcastAll("§6§l║ ⚽ VERLÄNGERUNG! ║"); + broadcastAll("§6§l╚══════════════════════╝"); + broadcastAll("§7Spielstand: §c" + redScore + " §7: §9" + blueScore); + + 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); } } + if (bossBar != null) { bossBar.setColor(BarColor.YELLOW); bossBar.setTitle("§6VERLÄNGERUNG §c" + redScore + " §7: §9" + blueScore); } + scoreboard.updateAll(); + } + + // ════════════════════════════════════════════════════════════════════════ + // ELFMETER-SCHIEßEN + // ════════════════════════════════════════════════════════════════════════ + + private void startPenalty() { + state = GameState.PENALTY; + penaltyRound = 1; penaltyRedGoals = 0; penaltyBlueGoals = 0; + penaltyRedShots = 0; penaltyBlueShots = 0; + penaltyTurn = Team.RED; penaltyShooterIndex = 0; + if (ball != null) { ball.remove(); ball = null; } + if (gameTask != null) { gameTask.cancel(); gameTask = null; } + + broadcastAll("§c§l╔═══════════════════════╗"); + broadcastAll("§c§l║ ⚽ ELFMETERSCHIEßEN! ║"); + broadcastAll("§c§l╚═══════════════════════╝"); + broadcastAll("§7Jedes Team schießt §e5 Elfmeter§7! Wer mehr trifft, gewinnt."); + + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { p.sendTitle("§c§lELFMETER!", "§75 Schüsse pro Team", 10, 80, 10); p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 2f, 2f); } + } + + if (bossBar != null) bossBar.setColor(BarColor.RED); + scoreboard.updateAll(); + + // Ersten Schuss nach 3s starten + new BukkitRunnable() { + public void run() { nextPenaltyShot(); } + }.runTaskLater(plugin, 60L); + } + + private void nextPenaltyShot() { + if (state != GameState.PENALTY) return; + penaltyGoalScored = false; + + // Prüfe ob noch geschossen werden muss + boolean roundsDone = penaltyRedShots >= penaltyMaxRounds && penaltyBlueShots >= penaltyMaxRounds; + if (roundsDone) { + if (penaltyRedGoals != penaltyBlueGoals) { + endGame(penaltyRedGoals > penaltyBlueGoals ? Team.RED : Team.BLUE); + return; + } + // Sudden Death: Runden erhöhen + penaltyMaxRounds++; + } + + // Schützen auswählen + List shooterList = penaltyTurn == Team.RED ? redTeam : blueTeam; + if (shooterList.isEmpty()) { endGame(getWinnerTeam()); return; } + UUID shooterUuid = shooterList.get(penaltyShooterIndex % shooterList.size()); + penaltyShooterIndex++; + + Player shooter = Bukkit.getPlayer(shooterUuid); + String teamColor = penaltyTurn == Team.RED ? "§c" : "§9"; + String teamName = penaltyTurn.getDisplayName(); + + broadcastAll("§e--- §7Schuss " + teamColor + teamName + " §8(§e" + (penaltyTurn == Team.RED ? penaltyRedShots + 1 : penaltyBlueShots + 1) + "/" + penaltyMaxRounds + "§8) §e---"); + + // Alle außer dem Schützen auf ihre Spawns + for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null && !p.equals(shooter)) p.teleport(arena.getRedSpawn()); } + for (UUID uuid : blueTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null && !p.equals(shooter)) p.teleport(arena.getBlueSpawn()); } + + // Ball aufstellen + if (ball != null) ball.remove(); + // KORREKTUR: Hier muss 'this' übergeben werden + ball = new Ball(this, plugin, arena.getBallSpawn()); + spawnCooldown = 0; // Elfmeter sofort spielbar + new BukkitRunnable() { + public void run() { ball.spawn(); lastBallLocation = arena.getBallSpawn().clone(); } + }.runTaskLater(plugin, 2L); + + // Schützen vorbereiten + if (shooter != null) { + shooter.teleport(arena.getBallSpawn()); + shooter.sendTitle(teamColor + "Du schießt!", "§7Du hast §e15 Sekunden§7!", 10, 40, 10); + shooter.sendMessage(MessageUtil.info("§eDu schießt den Elfmeter! Tritt auf den Ball!")); + } else { + broadcastAll(MessageUtil.warn("Schütze ist offline – Schuss übersprungen.")); + penaltyMiss(); return; + } + broadcastAll("§7" + (shooter.getName()) + " schießt für " + teamColor + teamName + "§7!"); + + if (bossBar != null) bossBar.setTitle("§c⚽ ELFMETER: §f" + shooter.getName() + " §c" + penaltyRedGoals + " §7: §9" + penaltyBlueGoals); + + // 15s-Timer + final Player finalShooter = shooter; + penaltyShotTask = new BukkitRunnable() { + int t = 15; + public void run() { + if (state != GameState.PENALTY || penaltyGoalScored) { cancel(); return; } + if (t <= 0) { penaltyMiss(); cancel(); return; } + if (t <= 5) finalShooter.sendMessage("§c⏱ " + t + "s!"); + t--; + } + }.runTaskTimer(plugin, 20L, 20L); + } + + /** Elfmeter-Tor wurde erzielt (von goalCheckLoop erkannt) */ + public void handlePenaltyGoal(Team scoredBy) { + if (state != GameState.PENALTY || penaltyGoalScored) return; + penaltyGoalScored = true; + if (penaltyShotTask != null) { penaltyShotTask.cancel(); penaltyShotTask = null; } + if (ball != null) { ball.remove(); ball = null; } + + // Punkt für das schießende Team + if (penaltyTurn == Team.RED) penaltyRedGoals++; else penaltyBlueGoals++; + if (penaltyTurn == Team.RED) penaltyRedShots++; else penaltyBlueShots++; + + String c = penaltyTurn == Team.RED ? "§c" : "§9"; + broadcastAll(c + "⚽ TREFFER! §7(" + penaltyRedGoals + ":" + penaltyBlueGoals + ")"); + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { spawnFirework(arena.getBallSpawn(), penaltyTurn); p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 2f, 1f); } + } + scoreboard.updateAll(); + advancePenaltyTurn(); + } + + private void penaltyMiss() { + if (penaltyTurn == Team.RED) penaltyRedShots++; else penaltyBlueShots++; + String c = penaltyTurn == Team.RED ? "§c" : "§9"; + broadcastAll(c + "✗ Kein Tor! §7(" + penaltyRedGoals + ":" + penaltyBlueGoals + ")"); + if (ball != null) { ball.remove(); ball = null; } + scoreboard.updateAll(); + advancePenaltyTurn(); + } + + private void advancePenaltyTurn() { + // Kann bereits entschieden werden? + if (canDecidePenalty()) { + new BukkitRunnable() { + public void run() { endGame(penaltyRedGoals > penaltyBlueGoals ? Team.RED : Team.BLUE); } + }.runTaskLater(plugin, 40L); + return; + } + penaltyTurn = penaltyTurn == Team.RED ? Team.BLUE : Team.RED; + new BukkitRunnable() { + public void run() { nextPenaltyShot(); } + }.runTaskLater(plugin, 40L); + } + + /** Prüft ob das Elfmeter-Ergebnis bereits feststeht (Early-Termination) */ + private boolean canDecidePenalty() { + int remaining = penaltyMaxRounds - Math.max(penaltyRedShots, penaltyBlueShots); + if (penaltyRedShots >= penaltyMaxRounds && penaltyBlueShots >= penaltyMaxRounds) return true; + // Kann der Rückstand noch aufgeholt werden? + int diff = Math.abs(penaltyRedGoals - penaltyBlueGoals); + if (diff > remaining) return true; + return false; + } + + // ════════════════════════════════════════════════════════════════════════ + // SPIEL-LOOP (1x pro Sekunde) + // ════════════════════════════════════════════════════════════════════════ + + private void startGameLoop() { + gameTask = new BukkitRunnable() { + public void run() { + // Nur bei RUNNING oder OVERTIME zählen + if (state != GameState.RUNNING && state != GameState.OVERTIME) return; + + timeLeft--; + secondsPlayed++; + scoreboard.updateAll(); + updateBossBar(); + checkPlayerBallInteraction(); + checkPlayerBoundaries(); + + // Freistoß-Abstandsdurchsetzung + if (freekickLocation != null) { + freekickTicks--; + enforceFreekickDistance(); + if (freekickTicks <= 0) { freekickLocation = null; throwInTeam = null; } + } + + // Warn-Nachrichten aus config + if (timeLeft == 60) broadcastAll(Messages.get("time-1min")); + if (timeLeft == 30) broadcastAll(Messages.get("time-30sec")); + if (timeLeft <= 10 && timeLeft > 0) { + broadcastAll("§c⏱ §l" + timeLeft + "!"); + 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 + if (timeLeft <= 0) { + if (state == GameState.OVERTIME) { + Team winner = getWinnerTeam(); + if (winner != null) { endGame(winner); } + else { startPenalty(); } + } else { + Team winner = getWinnerTeam(); + if (winner != null) { endGame(winner); } + else if (!overtimeDone) { startOvertime(); } + else { startPenalty(); } + } + cancel(); + } + } + }.runTaskTimer(plugin, 20L, 20L); + } + + // ════════════════════════════════════════════════════════════════════════ + // TOR + AUS CHECK (jeden Tick) + // ════════════════════════════════════════════════════════════════════════ + + private void startGoalCheckLoop() { + goalCheckTask = new BukkitRunnable() { + public void run() { + boolean active = state == GameState.RUNNING || state == GameState.OVERTIME || state == GameState.PENALTY; + if (!active) return; + + // ── Ball verschwunden? ─────────────────────────────────────── + if (ball == null || !ball.isActive() || ball.getEntity() == null || ball.getEntity().isDead()) { + ballMissingTicks++; + if (ballMissingTicks >= BALL_MISSING_TIMEOUT && state != GameState.GOAL) { + ballMissingTicks = 0; + plugin.getLogger().warning("[Fussball] Ball verschwunden – wird respawnt!"); + spawnBallDelayed(arena.getBallSpawn()); + } + return; + } + ballMissingTicks = 0; + + // ── Torwart: Ball-Position aktualisieren ───────────────────── + if (ball.isHeld()) { + ball.updateHeldPosition(); + lastBallLocation = ball.getEntity().getLocation().clone(); + return; // Kein Tor/Aus während gehalten + } + + // Spawn-Cooldown herunterzählen + if (spawnCooldown > 0) { + spawnCooldown--; + lastBallLocation = ball.getEntity().getLocation().clone(); + return; + } + + ball.applyFriction(); + + Location current = ball.getEntity().getLocation(); + if (lastBallLocation == null || !lastBallLocation.getWorld().equals(current.getWorld())) { + lastBallLocation = current.clone(); return; + } + + // 1. Tor-Check + Team scored = checkTrajectory(lastBallLocation, current); + if (scored != null) { + lastBallLocation = current.clone(); + offsideCooldown = false; + if (state == GameState.PENALTY) { handlePenaltyGoal(scored); } + else { scoreGoal(scored); } + return; + } + + // 2. Aus-Check (nur reguläres Spiel) + if (state != GameState.PENALTY && !outCooldown) { + String outSide = getOutSideTrajectory(lastBallLocation, current); + if (outSide != null) { + outCooldown = true; + offsideCooldown = false; + handleOutOfBounds(outSide, current); + lastBallLocation = current.clone(); + return; + } + } + + lastBallLocation = current.clone(); + } + }.runTaskTimer(plugin, 1L, 1L); + } + + private Team checkTrajectory(Location from, Location to) { + Vector diff = to.toVector().subtract(from.toVector()); + double distance = diff.length(); + int steps = Math.max(1, (int) Math.ceil(distance / 0.2)); + 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; + } + } + return null; + } + + private String getOutSideTrajectory(Location from, Location to) { + Vector diff = to.toVector().subtract(from.toVector()); + double distance = diff.length(); + int steps = Math.max(1, (int) Math.ceil(distance / 0.2)); + for (int i = 0; i <= steps; i++) { + double t = (double) i / steps; + Location p = from.clone().add(diff.clone().multiply(t)); + String side = arena.getOutSide(p); + if (side != null) return side; + } + return null; + } + + // ════════════════════════════════════════════════════════════════════════ + // AUS-BEHANDLUNG + // ════════════════════════════════════════════════════════════════════════ + + private void handleOutOfBounds(String side, Location outLocation) { + if (ball != null) ball.remove(); + // BUG FIX: lastTouchTeam kann null sein → neutrale Behandlung + Team touchTeam = lastTouchTeam; + Location resumeLocation; + String message; + + switch (side) { + case "side" -> { + resumeLocation = arena.clampToField(outLocation); + resumeLocation.setY(outLocation.getY()); + if (touchTeam == null) { + throwInTeam = null; + message = "§e⚽ §7Ball im Aus! §7Einwurf!"; + } else { + Team einwurfTeam = touchTeam.getOpponent(); + throwInTeam = einwurfTeam; + String other = einwurfTeam == Team.RED ? "§cRotes Team" : "§9Blaues Team"; + message = "§e⚽ §7Ball im Aus! §7Einwurf für " + other + "§7!"; + } + } + case "redEnd" -> { + if (touchTeam == Team.RED) { + resumeLocation = getCornerLocation(outLocation); + throwInTeam = Team.BLUE; + message = "§e⚽ §7Ball im Aus! §9Ecke für Blaues Team§7!"; + } else { + resumeLocation = arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn(); + throwInTeam = Team.RED; + message = "§e⚽ §7Ball im Aus! §cAbstoß für Rotes Team§7!"; + } + } + case "blueEnd" -> { + if (touchTeam == Team.BLUE) { + resumeLocation = getCornerLocation(outLocation); + throwInTeam = Team.RED; + message = "§e⚽ §7Ball im Aus! §cEcke für Rotes Team§7!"; + } else { + resumeLocation = arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn(); + throwInTeam = Team.BLUE; + message = "§e⚽ §7Ball im Aus! §9Abstoß für Blaues Team§7!"; + } + } + default -> { + resumeLocation = arena.getBallSpawn(); + throwInTeam = null; + message = "§e⚽ §7Ball im Aus!"; + } + } + + 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); } + + final Location spawnHere = resumeLocation; + new BukkitRunnable() { + public void run() { + if (state == GameState.RUNNING || state == GameState.OVERTIME) spawnBallDelayed(spawnHere); + } + }.runTaskLater(plugin, 40L); + } + + 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()); + 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 y = arena.getBallSpawn().getY(); + double cx = (Math.abs(outLoc.getX() - minX) < Math.abs(outLoc.getX() - maxX)) ? minX : maxX; + double cz = (Math.abs(outLoc.getZ() - minZ) < Math.abs(outLoc.getZ() - maxZ)) ? minZ : maxZ; + return new Location(outLoc.getWorld(), cx, y, cz); + } + + private void checkPlayerBallInteraction() { + if (ball == null || !ball.isActive()) return; + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p == null) continue; + if (ball.getDistanceTo(p) < 1.5) { + if (throwInTeam != null && getTeam(p) != throwInTeam) continue; + lastKicker = p.getUniqueId(); + lastTouchTeam = getTeam(p); + kicks.merge(p.getUniqueId(), 1, Integer::sum); + throwInTeam = null; + ball.kick(p); + } + } + } + + // ════════════════════════════════════════════════════════════════════════ + // TOR + // ════════════════════════════════════════════════════════════════════════ + + private void scoreGoal(Team scoringTeam) { + if (state != GameState.RUNNING && state != GameState.OVERTIME) return; + state = GameState.GOAL; + if (scoringTeam == Team.RED) redScore++; else blueScore++; + if (ball != null) { ball.remove(); ball = null; } + lastBallLocation = null; + + // Torschütze ermitteln + String scorerName = "Unbekannt"; + 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()); + } + lastKicker = null; + } + lastTouchTeam = null; // Reset nach Tor + + String color = scoringTeam == Team.RED ? "§c" : "§9"; + broadcastAll("§e§l╔══════════════════╗"); + broadcastAll("§e§l║ ⚽ T O R !! ║"); + broadcastAll("§e§l╚══════════════════╝"); + broadcastAll(color + "▶ " + scoringTeam.getDisplayName() + " §7hat ein Tor erzielt!"); + broadcastAll("§7Torschütze: §e" + scorerName); + 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; + 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)); + spawnFirework(arena.getBallSpawn(), scoringTeam); + p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 2f, 1f); + } + } + scoreboard.updateAll(); + updateBossBar(); + refreshSigns(); + logMatchEvent("§eTOR: §f" + scorerName + " §8[" + color + scoringTeam.getDisplayName() + "§8] §7" + redScore + ":" + blueScore); + + new BukkitRunnable() { + public void run() { + if (state == GameState.GOAL) { + state = GameState.RUNNING; + outOfBoundsCountdown.clear(); + spawnBallDelayed(arena.getBallSpawn()); + // BUG FIX: In 2. Halbzeit Seiten getauscht + 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()); } + } else { + for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getBlueSpawn()); } + for (UUID uuid : blueTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getRedSpawn()); } + } + broadcastAll(Messages.get("goal-continue")); + refreshSigns(); + } + } + }.runTaskLater(plugin, 80L); + } + + private void spawnFirework(Location loc, Team team) { + if (loc == null || loc.getWorld() == null) return; + FireworkEffect effect = FireworkEffect.builder() + .withColor(team == Team.RED ? org.bukkit.Color.RED : org.bukkit.Color.BLUE) + .withFade(org.bukkit.Color.WHITE).with(FireworkEffect.Type.BALL_LARGE).trail(true).build(); + Firework fw = loc.getWorld().spawn(loc, Firework.class); + FireworkMeta meta = fw.getFireworkMeta(); + meta.addEffect(effect); meta.setPower(1); fw.setFireworkMeta(meta); + } + + // ════════════════════════════════════════════════════════════════════════ + // BOSSBAR + // ════════════════════════════════════════════════════════════════════════ + + private void updateBossBar() { + if (bossBar == null) return; + int m = timeLeft / 60, s = timeLeft % 60; + String 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); + } + + // ════════════════════════════════════════════════════════════════════════ + // TORWART + // ════════════════════════════════════════════════════════════════════════ + + private void assignGoalkeepers() { + if (!redTeam.isEmpty()) { + redGoalkeeper = redTeam.get(0); + Player gk = Bukkit.getPlayer(redGoalkeeper); + if (gk != null) { gk.sendMessage(Messages.get("goalkeeper-assigned")); applyGoalkeeperArmor(gk, Team.RED); } + } + if (!blueTeam.isEmpty()) { + blueGoalkeeper = blueTeam.get(0); + Player gk = Bukkit.getPlayer(blueGoalkeeper); + if (gk != null) { gk.sendMessage(Messages.get("goalkeeper-assigned")); applyGoalkeeperArmor(gk, Team.BLUE); } + } + } + + private void applyGoalkeeperArmor(Player gk, Team team) { + org.bukkit.Color c = team == Team.RED + ? org.bukkit.Color.fromRGB(255, 140, 0) + : org.bukkit.Color.fromRGB(0, 180, 255); + ItemStack chest = new ItemStack(Material.LEATHER_CHESTPLATE); + if (chest.getItemMeta() instanceof org.bukkit.inventory.meta.LeatherArmorMeta meta) { + meta.setColor(c); + meta.setDisplayName((team == Team.RED ? "§c" : "§9") + "§l[TW]"); + chest.setItemMeta(meta); + } + gk.getInventory().setChestplate(chest); + } + + public boolean isGoalkeeper(Player player) { + UUID uuid = player.getUniqueId(); + return uuid.equals(redGoalkeeper) || uuid.equals(blueGoalkeeper); + } + + public boolean isInOwnHalf(Player player) { + if (arena.getFieldDirection() == null) return true; + double playerAxis = arena.getAxisValue(player.getLocation()); + double center = arena.getCenterAxisValue(); + Team team = getTeam(player); + if (team == null) return true; + double redGoalAxis = arena.getRedGoalAxisValue(); + double blueGoalAxis = arena.getBlueGoalAxisValue(); + boolean redIsLow = redGoalAxis < blueGoalAxis; + if (!secondHalf) { + return (team == Team.RED) ? (redIsLow ? playerAxis <= center : playerAxis >= center) + : (redIsLow ? playerAxis >= center : playerAxis <= center); + } else { + return (team == Team.RED) ? (redIsLow ? playerAxis >= center : playerAxis <= center) + : (redIsLow ? playerAxis <= center : playerAxis >= center); + } + } + + // ════════════════════════════════════════════════════════════════════════ + // ABSEITS + // ════════════════════════════════════════════════════════════════════════ + + private void checkOffside(UUID kickerUuid, Location ballLocation) { + if (arena.getFieldDirection() == null) return; + Player kicker = Bukkit.getPlayer(kickerUuid); if (kicker == null) return; + Team attackingTeam = getTeam(kicker); if (attackingTeam == null) return; + Team defendingTeam = attackingTeam.getOpponent(); + + double redGoalAxis = arena.getRedGoalAxisValue(); + double blueGoalAxis = arena.getBlueGoalAxisValue(); + double center = arena.getCenterAxisValue(); + double ballAxis = arena.getAxisValue(ballLocation); + boolean redAttacksHigh = blueGoalAxis > redGoalAxis; + boolean attackerGoesHigh = !secondHalf + ? (attackingTeam == Team.RED) == redAttacksHigh + : (attackingTeam == Team.RED) != redAttacksHigh; + + List defenders = defendingTeam == Team.RED ? redTeam : blueTeam; + List defPositions = new ArrayList<>(); + for (UUID uuid : defenders) { Player p = Bukkit.getPlayer(uuid); if (p != null) defPositions.add(arena.getAxisValue(p.getLocation())); } + if (defPositions.size() < 1) return; + if (attackerGoesHigh) defPositions.sort(Collections.reverseOrder()); else Collections.sort(defPositions); + double offsideLine = defPositions.size() >= 2 ? defPositions.get(1) : defPositions.get(0); + + List attackers = attackingTeam == Team.RED ? redTeam : blueTeam; + for (UUID uuid : attackers) { + if (uuid.equals(kickerUuid)) continue; + Player attacker = Bukkit.getPlayer(uuid); if (attacker == null) continue; + double aAxis = arena.getAxisValue(attacker.getLocation()); + boolean inOpponentHalf = attackerGoesHigh ? aAxis > center : aAxis < center; + boolean aheadOfDefender = attackerGoesHigh ? aAxis > offsideLine : aAxis < offsideLine; + boolean aheadOfBall = attackerGoesHigh ? aAxis > ballAxis : aAxis < ballAxis; + if (inOpponentHalf && aheadOfDefender && aheadOfBall) { + handleOffside(attacker, ballLocation, defendingTeam); return; + } + } + } + + private void handleOffside(Player offsidePlayer, Location ballLocation, Team freekickForTeam) { + offsideCooldown = true; + if (ball != null) ball.remove(); + lastBallLocation = null; + broadcastAll(Messages.get("offside", "player", offsidePlayer.getName(), "team", freekickForTeam.getDisplayName())); + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { + p.sendTitle(Messages.get("offside-title"), Messages.get("offside-sub", "team", freekickForTeam.getDisplayName()), 5, 40, 10); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 0.8f); + } + } + logMatchEvent("§7Abseits: §e" + offsidePlayer.getName()); + startFreekick(freekickForTeam, ballLocation, Messages.get("offside-title")); + new BukkitRunnable() { public void run() { offsideCooldown = false; } }.runTaskLater(plugin, 60L); + } + + // ════════════════════════════════════════════════════════════════════════ + // FOULS & KARTEN + // ════════════════════════════════════════════════════════════════════════ + + public void handleFoul(Player fouler, Player victim, Location foulLocation, boolean directRed) { + if (state != GameState.RUNNING && state != GameState.OVERTIME) return; + Team victimTeam = getTeam(victim); if (victimTeam == null) return; + broadcastAll(Messages.get("foul", "player", fouler.getName())); + 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())); + logMatchEvent("§cFoul: §e" + fouler.getName() + " §7→ §e" + victim.getName()); + } + + private void giveYellowCard(Player player, String reason) { + UUID uuid = player.getUniqueId(); + int yellows = yellowCards.merge(uuid, 1, Integer::sum); + if (yellows >= 2) { + 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()); + new BukkitRunnable() { public void run() { disqualifyPlayer(player); } }.runTaskLater(plugin, 60L); + } else { + broadcastAll(Messages.get("yellow-card", "player", player.getName(), "reason", reason)); + player.sendTitle("§e§l🟨", "§7Gelbe Karte!", 5, 60, 10); + player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 0.6f); + logMatchEvent("§e🟨 §7Gelb: §e" + player.getName() + " §8(" + reason + ")"); + } + scoreboard.updateAll(); + } + + private void giveRedCard(Player player, String reason) { + redCards.add(player.getUniqueId()); + broadcastAll(Messages.get("red-card", "player", player.getName(), "reason", reason)); + 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 + ")"); + new BukkitRunnable() { public void run() { disqualifyPlayer(player); } }.runTaskLater(plugin, 80L); + } + + public int getYellowCards(Player p) { return yellowCards.getOrDefault(p.getUniqueId(), 0); } + public boolean hasRedCard(Player p) { return redCards.contains(p.getUniqueId()); } + + // ════════════════════════════════════════════════════════════════════════ + // FREISTOUSS + // ════════════════════════════════════════════════════════════════════════ + + private void startFreekick(Team executingTeam, Location ballLoc, String reason) { + spawnBallDelayed(ballLoc); + throwInTeam = executingTeam; + freekickLocation = ballLoc.clone(); + freekickTicks = plugin.getConfig().getInt("gameplay.freekick-duration", 600); + String teamColor = executingTeam == Team.RED ? "§c" : "§9"; + broadcastAll(Messages.get("freekick", "team", teamColor + executingTeam.getDisplayName())); + double dist = plugin.getConfig().getDouble("gameplay.freekick-distance", 5.0); + broadcastAll(Messages.get("freekick-hint", "n", String.format("%.0f", dist))); + } + + private void enforceFreekickDistance() { + if (freekickLocation == null || throwInTeam == null) return; + double minDist = plugin.getConfig().getDouble("gameplay.freekick-distance", 5.0); + Team opposingTeam = throwInTeam.getOpponent(); + List opponents = opposingTeam == Team.RED ? redTeam : blueTeam; + for (UUID uuid : opponents) { + Player p = Bukkit.getPlayer(uuid); if (p == null) continue; + if (!p.getWorld().equals(freekickLocation.getWorld())) continue; + if (p.getLocation().distance(freekickLocation) < minDist) { + org.bukkit.util.Vector away = p.getLocation().toVector().subtract(freekickLocation.toVector()).setY(0); + if (away.lengthSquared() < 0.001) away = new org.bukkit.util.Vector(1, 0, 0); + p.setVelocity(away.normalize().multiply(0.6)); + p.sendMessage(Messages.get("freekick-push", "n", String.format("%.0f", minDist))); + } + } + } + + // ════════════════════════════════════════════════════════════════════════ + // SPIELER-FELDGRENZEN + // ════════════════════════════════════════════════════════════════════════ + + private void checkPlayerBoundaries() { + if (arena.getFieldMin() == null || arena.getFieldMax() == null) return; + if (state != GameState.RUNNING && state != GameState.OVERTIME) return; + double tolerance = plugin.getConfig().getDouble("gameplay.out-of-bounds-tolerance", 2.0); + int maxSecs = plugin.getConfig().getInt("gameplay.out-of-bounds-countdown", 5); + for (UUID uuid : new ArrayList<>(allPlayers)) { + Player p = Bukkit.getPlayer(uuid); if (p == null) continue; + double distOut = getDistanceOutsideField(p.getLocation()); + if (distOut <= tolerance) { + if (outOfBoundsCountdown.containsKey(uuid)) { + outOfBoundsCountdown.remove(uuid); + p.sendMessage(Messages.get("boundary-return")); + } + continue; + } + if (!outOfBoundsCountdown.containsKey(uuid)) { + outOfBoundsCountdown.put(uuid, maxSecs); + p.sendTitle("§c⚠ SPIELFELDGRENZE!", "§7Zurück in §e" + maxSecs + " §7Sek!", 5, 30, 5); + p.sendMessage(Messages.get("boundary-warn", "n", String.valueOf(maxSecs))); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 0.5f); + } else { + int remaining = outOfBoundsCountdown.get(uuid) - 1; + if (remaining <= 0) { outOfBoundsCountdown.remove(uuid); disqualifyPlayer(p); } + else { + outOfBoundsCountdown.put(uuid, remaining); + p.sendTitle("§c⚠ §l" + remaining + "s", "§7Zurück ins Spielfeld!", 0, 25, 5); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 0.5f + (maxSecs - remaining) * 0.1f); + } + } + } + } + + private double getDistanceOutsideField(Location loc) { + if (arena.getFieldMin() == null || arena.getFieldMax() == null) return 0; + 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 dx = 0, dz = 0; + if (loc.getX() < minX) dx = minX - loc.getX(); else if (loc.getX() > maxX) dx = loc.getX() - maxX; + if (loc.getZ() < minZ) dz = minZ - loc.getZ(); else if (loc.getZ() > maxZ) dz = loc.getZ() - maxZ; + return Math.sqrt(dx * dx + dz * dz); + } + + private void disqualifyPlayer(Player player) { + UUID uuid = player.getUniqueId(); + if (!isInGame(player)) return; + player.sendTitle("§c§lDISQUALIFIZIERT!", "§7Du wurdest vom Platz gestellt!", 10, 80, 20); + player.sendMessage(Messages.get("boundary-disq-self")); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f); + broadcastAll(Messages.get("boundary-disq", "player", player.getName())); + allPlayers.remove(uuid); redTeam.remove(uuid); blueTeam.remove(uuid); + outOfBoundsCountdown.remove(uuid); + if (uuid.equals(redGoalkeeper)) redGoalkeeper = redTeam.isEmpty() ? null : redTeam.get(0); + if (uuid.equals(blueGoalkeeper)) blueGoalkeeper = blueTeam.isEmpty() ? null : blueTeam.get(0); + if (bossBar != null) bossBar.removePlayer(player); + scoreboard.remove(player); + resetPlayer(player); + if (allPlayers.size() < arena.getMinPlayers()) { endGame(getWinnerTeam()); return; } + scoreboard.updateAll(); refreshSigns(); + } + + // ════════════════════════════════════════════════════════════════════════ + // MATCHBERICHT + // ════════════════════════════════════════════════════════════════════════ + + private void logMatchEvent(String event) { + int minute = Math.max(1, secondsPlayed / 60 + 1); + matchEvents.add("§8" + minute + "' §r" + event); + } + + private void sendMatchReport() { + broadcastAll(Messages.get("report-header")); + broadcastAll("§7Endergebnis: §c" + redScore + " §7: §9" + blueScore); + + List goalLines = matchEvents.stream().filter(e -> e.contains("TOR:")).collect(Collectors.toList()); + List cardLines = matchEvents.stream().filter(e -> e.contains("🟨") || e.contains("🟥")).collect(Collectors.toList()); + List foulLines = matchEvents.stream().filter(e -> e.contains("Foul")).collect(Collectors.toList()); + List offsideLines = matchEvents.stream().filter(e -> e.contains("Abseits")).collect(Collectors.toList()); + + if (!goalLines.isEmpty()) { broadcastAll(Messages.get("report-goals")); goalLines.forEach(this::broadcastAll); } + if (!cardLines.isEmpty()) { broadcastAll(Messages.get("report-cards")); cardLines.forEach(this::broadcastAll); } + if (!foulLines.isEmpty()) { broadcastAll(Messages.get("report-fouls")); foulLines.forEach(this::broadcastAll); } + if (!offsideLines.isEmpty()) { broadcastAll(Messages.get("report-offside")); offsideLines.forEach(this::broadcastAll); } + if (matchEvents.isEmpty()) { broadcastAll(Messages.get("report-no-events")); } + + UUID mvpUuid = goals.entrySet().stream().max(Map.Entry.comparingByValue()).map(Map.Entry::getKey).orElse(null); + if (mvpUuid != null) { + Player mvp = Bukkit.getPlayer(mvpUuid); + broadcastAll(Messages.get("report-mvp", "player", mvp != null ? mvp.getName() : "?", "n", String.valueOf(goals.get(mvpUuid)))); + } + broadcastAll(Messages.get("report-footer")); + } + + // ════════════════════════════════════════════════════════════════════════ + // SPIELENDE + // ════════════════════════════════════════════════════════════════════════ + + public void endGame(Team winner) { + if (state == GameState.ENDING) return; + state = GameState.ENDING; + if (gameTask != null) gameTask.cancel(); + if (countdownTask!= null) countdownTask.cancel(); + if (goalCheckTask!= null) goalCheckTask.cancel(); + if (penaltyShotTask != null) penaltyShotTask.cancel(); + if (ball != null) ball.remove(); + if (bossBar != null) { bossBar.removeAll(); bossBar = null; } + outOfBoundsCountdown.clear(); + freekickLocation = null; + freekickTicks = 0; + throwInTeam = null; + + // Persistente Statistiken speichern + boolean draw = (winner == null || redScore == blueScore) && state != GameState.PENALTY; + Map names = new HashMap<>(); + for (UUID uuid : allPlayers) { Player p = Bukkit.getPlayer(uuid); if (p != null) names.put(uuid, p.getName()); } + plugin.getStatsManager().flushKicks(kicks, names); + for (UUID uuid : allPlayers) { + Player p = Bukkit.getPlayer(uuid); + if (p == null) continue; + Team t = getTeam(p); + StatsManager.GameResult result; + if (draw) result = StatsManager.GameResult.DRAW; + else if (t == winner) result = StatsManager.GameResult.WIN; + else result = StatsManager.GameResult.LOSS; + plugin.getStatsManager().addGameResult(uuid, p.getName(), result); + } + + // Matchbericht senden + sendMatchReport(); + + // Ergebnis-Nachrichten + broadcastAll("§e§l╔══════════════════════╗"); + if (winner == null) { + broadcastAll("§7§l║ UNENTSCHIEDEN! ║"); + } else { + String c = winner == Team.RED ? "§c" : "§9"; + broadcastAll(c + "§l║ " + winner.getDisplayName().toUpperCase() + " GEWINNT! ║"); + } + broadcastAll("§e§l╚══════════════════════╝"); + broadcastAll("§7Endergebnis: §c" + redScore + " §7: §9" + blueScore); + if (state == GameState.ENDING && penaltyRedGoals + penaltyBlueGoals > 0) { + broadcastAll("§7Elfmeter: §c" + penaltyRedGoals + " §7: §9" + penaltyBlueGoals); + } + + UUID mvpUuid = goals.entrySet().stream().max(Map.Entry.comparingByValue()).map(Map.Entry::getKey).orElse(null); + if (mvpUuid != null) { + Player mvp = Bukkit.getPlayer(mvpUuid); + if (mvp != null) broadcastAll("§6⭐ MVP: §e" + mvp.getName() + " §7(" + goals.get(mvpUuid) + " Tore)"); + } + + for (UUID uuid : getAllAndSpectators()) { + Player p = Bukkit.getPlayer(uuid); + if (p == null) continue; + if (winner == null) p.sendTitle("§7UNENTSCHIEDEN", "§7" + redScore + " : " + blueScore, 10, 80, 20); + else if (getTeam(p) == winner) p.sendTitle("§6§lGEWONNEN! 🏆", "§7Herzlichen Glückwunsch!", 10, 80, 20); + else if (isSpectator(p)) p.sendTitle("§7SPIEL BEENDET", "§7" + redScore + " : " + blueScore, 10, 80, 20); + else p.sendTitle("§c§lVERLOREN!", "§7Gutes Spiel!", 10, 80, 20); + p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1f); + } + + new BukkitRunnable() { + public void run() { + for (UUID uuid : new ArrayList<>(allPlayers)) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { resetPlayer(p); scoreboard.remove(p); } + } + for (UUID uuid : new ArrayList<>(spectators)) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { resetPlayer(p); scoreboard.remove(p); } + } + allPlayers.clear(); redTeam.clear(); blueTeam.clear(); spectators.clear(); + redScore = 0; blueScore = 0; timeLeft = arena.getGameDuration(); + secondHalf = false; overtimeDone = false; + state = GameState.WAITING; + plugin.getGameManager().removeGame(arena.getName()); + refreshSigns(); + } + }.runTaskLater(plugin, 100L); + } + + // ════════════════════════════════════════════════════════════════════════ + // TEAM-CHAT + // ════════════════════════════════════════════════════════════════════════ + + public void sendTeamMessage(Player sender, String message) { + Team team = getTeam(sender); + if (team == null) return; + String prefix = team == Team.RED ? "§c[ROT] " : "§9[BLAU] "; + String formatted = prefix + "§f" + sender.getName() + "§7: " + message; + List teamList = team == Team.RED ? redTeam : blueTeam; + for (UUID uuid : teamList) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.sendMessage(formatted); } + // Zuschauer sehen alle Chats (mit Label) + for (UUID uuid : spectators) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.sendMessage("§8[Zuschauer] " + formatted); } + plugin.getLogger().info("[TeamChat][" + team.getDisplayName() + "] " + sender.getName() + ": " + message); + } + + // ════════════════════════════════════════════════════════════════════════ + // HILFSMETHODEN + // ════════════════════════════════════════════════════════════════════════ + + private void spawnBallDelayed(Location spawnAt) { + if (spawnAt == null) { plugin.getLogger().severe("[Fussball] Ball-Spawn null!"); return; } + if (ball != null) ball.remove(); + // KORREKTUR: Hier muss 'this' übergeben werden + ball = new Ball(this, plugin, spawnAt); + lastBallLocation = spawnAt.clone(); + outCooldown = false; + throwInTeam = null; // Nach Tor/Anstoß darf wieder jeder + spawnCooldown = 60; // BUG FIX: 3s Schonfrist nach Ball-Spawn + new BukkitRunnable() { + public void run() { spawnAt.getWorld().loadChunk(spawnAt.getChunk()); ball.spawn(); lastBallLocation = spawnAt.clone(); } + }.runTaskLater(plugin, 2L); + } + + private Team getWinnerTeam() { + if (redScore > blueScore) return Team.RED; + if (blueScore > redScore) return Team.BLUE; + return null; + } + + private List getAllAndSpectators() { + List all = new ArrayList<>(allPlayers); + all.addAll(spectators); + return all; + } + + private void refreshSigns() { plugin.getSignListener().refreshSignsForArena(arena.getName()); } + + /** Broadcast an alle Spieler UND Zuschauer */ + public void broadcastAll(String msg) { + for (UUID uuid : getAllAndSpectators()) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.sendMessage(msg); } + } + + // ════════════════════════════════════════════════════════════════════════ + // TORWART-REGEL: BALL HALTEN + // ════════════════════════════════════════════════════════════════════════ + + /** + * Prüft, ob ein Spieler nahe genug am Tor ist, um den Ball zu halten. + * Erlaubter Radius: 2 Blöcke um das definierte Tor herum. + */ + public boolean isAllowedToHoldBall(Player player) { + Location pLoc = player.getLocation(); + double maxDistance = 2.0; + + // Prüfen ob Spieler im Roten Tor-Bereich (inkl. 2 Block Radius) ist + if (arena.getRedGoalMin() != null && arena.getRedGoalMax() != null) { + if (getDistanceToBox(pLoc, arena.getRedGoalMin(), arena.getRedGoalMax()) <= maxDistance) { + return true; + } + } + + // Prüfen ob Spieler im Blauen Tor-Bereich (inkl. 2 Block Radius) ist + if (arena.getBlueGoalMin() != null && arena.getBlueGoalMax() != null) { + if (getDistanceToBox(pLoc, arena.getBlueGoalMin(), arena.getBlueGoalMax()) <= maxDistance) { + return true; + } + } + + return false; + } + + /** + * Berechnet den kürzesten Abstand eines Punktes (Spieler) zu einer Box (Torrahmen). + */ + private double getDistanceToBox(Location loc, Location min, Location max) { + // Wir clampen die Spieler-Koordinaten auf die Box- Grenzen, um den nächsten Punkt auf der Box zu finden + double x = Math.max(min.getX(), Math.min(loc.getX(), max.getX())); + double y = Math.max(min.getY(), Math.min(loc.getY(), max.getY())); + double z = Math.max(min.getZ(), Math.min(loc.getZ(), max.getZ())); + + // Abstand vom Spieler zu diesem Punkt berechnen + double dx = loc.getX() - x; + double dy = loc.getY() - y; + double dz = loc.getZ() - z; + + return Math.sqrt(dx*dx + dy*dy + dz*dz); + } + + // ════════════════════════════════════════════════════════════════════════ + // GETTER / SETTER + // ════════════════════════════════════════════════════════════════════════ + + public Team getTeam(Player player) { + UUID uuid = player.getUniqueId(); + if (redTeam.contains(uuid)) return Team.RED; + if (blueTeam.contains(uuid)) return Team.BLUE; + return null; + } + + public boolean isInGame(Player player) { return allPlayers.contains(player.getUniqueId()); } + + public void setLastKicker(UUID uuid) { + this.lastKicker = uuid; + 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) + 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()); + } + } + + public Arena getArena() { return arena; } + public GameState getState() { return state; } + public List getRedTeam() { return redTeam; } + public List getBlueTeam() { return blueTeam; } + public List getAllPlayers() { return allPlayers; } + public int getRedScore() { return redScore; } + public int getBlueScore() { return blueScore; } + public int getTimeLeft() { return timeLeft; } + public Ball getBall() { return ball; } + public Map getGoals() { return goals; } + public Map getKicks() { return kicks; } + public boolean isSecondHalf() { return secondHalf; } + public int getPenaltyRedGoals() { return penaltyRedGoals; } + public int getPenaltyBlueGoals() { return penaltyBlueGoals; } + /** Gibt zurück welches Team gerade Einwurf/Ecke/Abstoß hat (null = jeder darf) */ + public Team getThrowInTeam() { return throwInTeam; } + /** Berechtigung aufheben – wird von BallListener nach dem ersten Schuss gerufen */ + public void clearThrowIn() { + throwInTeam = null; + freekickLocation = null; + freekickTicks = 0; + offsideCooldown = false; + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/game/GameManager.java b/src/main/java/de/fussball/plugin/game/GameManager.java new file mode 100644 index 0000000..c7b99c2 --- /dev/null +++ b/src/main/java/de/fussball/plugin/game/GameManager.java @@ -0,0 +1,133 @@ +package de.fussball.plugin.game; + +import de.fussball.plugin.Fussball; +import de.fussball.plugin.arena.Arena; +import de.fussball.plugin.utils.MessageUtil; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.*; + +public class GameManager { + + private final Fussball plugin; + private final Map games = new HashMap<>(); + // Warteschlange pro Arena: FIFO-Queue mit Spieler-UUIDs + private final Map> queues = new HashMap<>(); + + public GameManager(Fussball plugin) { this.plugin = plugin; } + + // ── Spiel-Verwaltung ──────────────────────────────────────────────────── + + public Game createGame(Arena arena) { + String key = arena.getName().toLowerCase(); + if (games.containsKey(key)) return games.get(key); + Game game = new Game(plugin, arena); + games.put(key, game); + return game; + } + + public Game getGame(String arenaName) { return games.get(arenaName.toLowerCase()); } + public void removeGame(String arenaName) { + games.remove(arenaName.toLowerCase()); + // Warteschlange abarbeiten – nächster Spieler darf joinen + processQueue(arenaName); + } + + public Collection getAllGames() { return games.values(); } + + public Game getPlayerGame(Player player) { + for (Game game : games.values()) if (game.isInGame(player)) return game; + return null; + } + + public boolean isInGame(Player player) { return getPlayerGame(player) != null; } + + /** Gibt zurück ob der Spieler als Zuschauer in einem Spiel sitzt */ + public Game getSpectatorGame(Player player) { + for (Game game : games.values()) if (game.isSpectator(player)) return game; + return null; + } + + public boolean isInAnyGame(Player player) { + return isInGame(player) || getSpectatorGame(player) != null; + } + + public void stopAllGames() { + for (Game game : new ArrayList<>(games.values())) game.endGame(null); + games.clear(); + queues.clear(); + } + + // ── Warteschlange ─────────────────────────────────────────────────────── + + /** Spieler zur Warteschlange einer Arena hinzufügen */ + public void addToQueue(String arenaName, Player player) { + String key = arenaName.toLowerCase(); + queues.computeIfAbsent(key, k -> new LinkedList<>()); + Queue queue = queues.get(key); + if (queue.contains(player.getUniqueId())) { + player.sendMessage(MessageUtil.warn("Du bist bereits in der Warteschlange für §e" + arenaName + "§e!")); + return; + } + queue.add(player.getUniqueId()); + int pos = getQueuePosition(arenaName, player); + player.sendMessage(MessageUtil.info("Du bist in der Warteschlange für §e" + arenaName + " §7(Position §e" + pos + "§7)")); + } + + /** Spieler aus der Warteschlange entfernen */ + public void removeFromQueue(String arenaName, Player player) { + Queue queue = queues.get(arenaName.toLowerCase()); + if (queue != null) queue.remove(player.getUniqueId()); + } + + /** Entfernt einen Spieler aus ALLEN Warteschlangen */ + public void removeFromAllQueues(Player player) { + for (Queue q : queues.values()) q.remove(player.getUniqueId()); + } + + public int getQueuePosition(String arenaName, Player player) { + Queue queue = queues.get(arenaName.toLowerCase()); + if (queue == null) return -1; + int pos = 1; + for (UUID uuid : queue) { + if (uuid.equals(player.getUniqueId())) return pos; + pos++; + } + return -1; + } + + public int getQueueSize(String arenaName) { + Queue queue = queues.get(arenaName.toLowerCase()); + return queue == null ? 0 : queue.size(); + } + + /** + * Wenn ein Spiel endet oder Platz frei wird → nächsten Spieler aus der + * Warteschlange in das (neue) Spiel einladen. + */ + private void processQueue(String arenaName) { + String key = arenaName.toLowerCase(); + Queue queue = queues.get(key); + if (queue == null || queue.isEmpty()) return; + + Arena arena = plugin.getArenaManager().getArena(arenaName); + if (arena == null || !arena.isSetupComplete()) return; + + // Warte kurz, bis das alte Spiel vollständig aufgeräumt ist + org.bukkit.scheduler.BukkitRunnable task = new org.bukkit.scheduler.BukkitRunnable() { + public void run() { + Queue 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); + } + }; + task.runTaskLater(plugin, 120L); // 6s nach Spielende + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/game/GameState.java b/src/main/java/de/fussball/plugin/game/GameState.java new file mode 100644 index 0000000..0b65eb2 --- /dev/null +++ b/src/main/java/de/fussball/plugin/game/GameState.java @@ -0,0 +1,12 @@ +package de.fussball.plugin.game; + +public enum GameState { + WAITING, // Warte auf Spieler + STARTING, // Countdown läuft + RUNNING, // Spiel läuft + GOAL, // Tor-Pause + HALFTIME, // Halbzeit-Pause + OVERTIME, // Verlängerung + PENALTY, // Elfmeterschießen + ENDING // Spiel beendet +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/game/Team.java b/src/main/java/de/fussball/plugin/game/Team.java new file mode 100644 index 0000000..10a7440 --- /dev/null +++ b/src/main/java/de/fussball/plugin/game/Team.java @@ -0,0 +1,22 @@ +package de.fussball.plugin.game; +import org.bukkit.ChatColor; + +public enum Team { + RED("Rot", ChatColor.RED, "§c"), + BLUE("Blau", ChatColor.BLUE, "§9"); + + private final String displayName; + private final ChatColor color; + private final String colorCode; + + Team(String displayName, ChatColor color, String colorCode) { + this.displayName = displayName; + this.color = color; + this.colorCode = colorCode; + } + + public String getDisplayName() { return displayName; } + public ChatColor getColor() { return color; } + public String getColorCode() { return colorCode; } + public Team getOpponent() { return this == RED ? BLUE : RED; } +} diff --git a/src/main/java/de/fussball/plugin/listeners/BallListener.java b/src/main/java/de/fussball/plugin/listeners/BallListener.java new file mode 100644 index 0000000..3f3b220 --- /dev/null +++ b/src/main/java/de/fussball/plugin/listeners/BallListener.java @@ -0,0 +1,222 @@ +package de.fussball.plugin.listeners; + +import de.fussball.plugin.Fussball; +import de.fussball.plugin.game.Ball; +import de.fussball.plugin.game.Game; +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.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.bukkit.event.player.PlayerToggleSneakEvent; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Verwaltet Ball-Interaktionen: + * • Normaler Schuss – Rechtsklick auf Ball + * • Aufgeladener Schuss – Shift halten → loslassen + * • Torwart – Rechtsklick = halten, nochmal Rechtsklick/Shift = werfen + * • Foul-Erkennung – Spieler trifft gegnerischen Spieler + */ +public class BallListener implements Listener { + + private final Fussball plugin; + // UUID → Timestamp Lade-Beginn (ms) + private final Map chargeMap = new HashMap<>(); + + public BallListener(Fussball plugin) { this.plugin = plugin; } + + // ── Rechtsklick auf Ball ───────────────────────────────────────────────── + + @EventHandler + public void onInteract(PlayerInteractAtEntityEvent event) { + if (!(event.getRightClicked() instanceof ArmorStand stand)) return; + Player player = event.getPlayer(); + Game game = plugin.getGameManager().getPlayerGame(player); + if (game == null) return; + Ball ball = game.getBall(); + if (ball == null || !stand.equals(ball.getEntity())) return; + event.setCancelled(true); + if (game.getState() != GameState.RUNNING && game.getState() != GameState.OVERTIME) return; + + // ── Torwart hält Ball bereits → werfen ────────────────────────────── + if (ball.isHeld() && player.equals(ball.getHoldingPlayer())) { + double power = plugin.getConfig().getDouble("gameplay.goalkeeper-throw-power", 1.8); + game.setLastKicker(player.getUniqueId()); + ball.throwBall(player, power); + game.clearThrowIn(); + game.broadcastAll(plugin.getConfig().getString("messages.goalkeeper-throw", + "§6TW §f{player} §7wirft den Ball!").replace("{player}", player.getName())); + return; + } + + // ── 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 + } 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)) { + 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; + } + } 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 + } + } + } + + // ── Normaler Schuss (kein Shift) ──────────────────────────────────── + if (player.isSneaking()) return; + if (game.getThrowInTeam() != null && game.getThrowInTeam() != game.getTeam(player)) { + player.sendMessage("§cDu bist nicht dran! Warte auf den Einwurf des anderen Teams."); + return; + } + game.clearThrowIn(); + game.setLastKicker(player.getUniqueId()); + ball.kick(player); + } + + // ── Linksklick / Schaden am Ball ──────────────────────────────────────── + + @EventHandler + public void onDamage(EntityDamageByEntityEvent event) { + if (!(event.getEntity() instanceof ArmorStand)) return; + if (!(event.getDamager() instanceof Player player)) return; + Game game = plugin.getGameManager().getPlayerGame(player); + if (game == null) return; + Ball ball = game.getBall(); + if (ball == null || !ball.getEntity().equals(event.getEntity())) return; + event.setCancelled(true); + if (game.getState() != GameState.RUNNING && game.getState() != GameState.OVERTIME) return; + if (player.isSneaking()) return; + if (game.getThrowInTeam() != null && game.getThrowInTeam() != game.getTeam(player)) { + player.sendMessage("§cDu bist nicht dran!"); + return; + } + game.clearThrowIn(); + game.setLastKicker(player.getUniqueId()); + ball.kick(player); + } + + // ── Foul-Erkennung (Spieler trifft gegnerischen Spieler) ───────────────── + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPlayerHitPlayer(EntityDamageByEntityEvent event) { + if (!(event.getEntity() instanceof Player victim)) return; + Player damager; + if (event.getDamager() instanceof Player p) { + damager = p; + } else if (event.getDamager() instanceof Projectile proj && proj.getShooter() instanceof Player p) { + damager = p; + } else return; + + if (!plugin.getConfig().getBoolean("gameplay.foul-detection-enabled", true)) return; + + Game game = plugin.getGameManager().getPlayerGame(damager); + if (game == null || !game.isInGame(victim)) return; + if (game.getState() != GameState.RUNNING && game.getState() != GameState.OVERTIME) return; + + Team damagerTeam = game.getTeam(damager); + Team victimTeam = game.getTeam(victim); + if (damagerTeam == null || victimTeam == null || damagerTeam == victimTeam) return; + + // Schaden canceln (kein PvP im Fußball) + event.setCancelled(true); + + // Foul registrieren + double damage = event.getFinalDamage(); + boolean directRedCard = damage >= 8.0; // Sehr harter Schlag → direkt Rot + game.handleFoul(damager, victim, victim.getLocation(), directRedCard); + } + + // ── Aufgeladener Schuss (Shift-System) ────────────────────────────────── + + @EventHandler + public void onSneak(PlayerToggleSneakEvent event) { + Player player = event.getPlayer(); + Game game = plugin.getGameManager().getPlayerGame(player); + if (game == null) return; + if (game.getState() != GameState.RUNNING && game.getState() != GameState.OVERTIME) { + chargeMap.remove(player.getUniqueId()); return; + } + + Ball ball = game.getBall(); + + // ── Torwart hält Ball → Shift loslassen = werfen ──────────────────── + if (!event.isSneaking() && ball != null && ball.isHeld() && player.equals(ball.getHoldingPlayer())) { + double power = plugin.getConfig().getDouble("gameplay.goalkeeper-throw-power", 1.8); + game.setLastKicker(player.getUniqueId()); + ball.throwBall(player, power); + game.clearThrowIn(); + game.broadcastAll(plugin.getConfig().getString("messages.goalkeeper-throw", + "§6TW §f{player} §7wirft den Ball!").replace("{player}", player.getName())); + return; + } + + if (event.isSneaking()) { + // ── Shift gedrückt: Laden beginnen ────────────────────────────── + if (ball == null || ball.getDistanceTo(player) > 2.5) return; + chargeMap.put(player.getUniqueId(), System.currentTimeMillis()); + startChargeDisplay(player, game); + } else { + // ── Shift losgelassen: Schuss abfeuern ────────────────────────── + Long startTime = chargeMap.remove(player.getUniqueId()); + if (startTime == null) return; + if (ball == null || ball.getDistanceTo(player) > 2.5) return; + if (game.getThrowInTeam() != null && game.getThrowInTeam() != game.getTeam(player)) { + player.sendMessage("§cDu bist nicht dran!"); + return; + } + game.clearThrowIn(); + long held = System.currentTimeMillis() - startTime; + double power = Math.min(held / 1500.0, 1.0); + game.setLastKicker(player.getUniqueId()); + ball.chargedKick(player, power); + } + } + + private void startChargeDisplay(Player player, Game game) { + new BukkitRunnable() { + @Override + public void run() { + if (!player.isSneaking() || !chargeMap.containsKey(player.getUniqueId())) { cancel(); return; } + if (game.getState() != GameState.RUNNING && game.getState() != GameState.OVERTIME) { + chargeMap.remove(player.getUniqueId()); cancel(); return; + } + Ball ball = game.getBall(); + if (ball == null || ball.getDistanceTo(player) > 2.5) { + chargeMap.remove(player.getUniqueId()); cancel(); return; + } + long elapsed = System.currentTimeMillis() - chargeMap.get(player.getUniqueId()); + double power = Math.min(elapsed / 1500.0, 1.0); + int filled = (int) Math.round(power * 10); + String color = power < 0.4 ? "§a" : power < 0.8 ? "§e" : "§c"; + String bar = color + "█".repeat(filled) + "§8" + "█".repeat(10 - filled); + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, + TextComponent.fromLegacyText("§e⚽ Schuss-Power: " + bar + " §f" + (int)(power*100) + "%")); + if (power >= 1.0) { + chargeMap.remove(player.getUniqueId()); + game.setLastKicker(player.getUniqueId()); + ball.chargedKick(player, 1.0); + cancel(); + } + } + }.runTaskTimer(plugin, 1L, 2L); + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/listeners/BlockListener.java b/src/main/java/de/fussball/plugin/listeners/BlockListener.java new file mode 100644 index 0000000..4eb3100 --- /dev/null +++ b/src/main/java/de/fussball/plugin/listeners/BlockListener.java @@ -0,0 +1,22 @@ +package de.fussball.plugin.listeners; + +import de.fussball.plugin.Fussball; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; + +public class BlockListener implements Listener { + private final Fussball plugin; + public BlockListener(Fussball plugin) { this.plugin = plugin; } + + @EventHandler + public void onBreak(BlockBreakEvent event) { + if (plugin.getGameManager().isInGame(event.getPlayer())) event.setCancelled(true); + } + + @EventHandler + public void onPlace(BlockPlaceEvent event) { + if (plugin.getGameManager().isInGame(event.getPlayer())) event.setCancelled(true); + } +} diff --git a/src/main/java/de/fussball/plugin/listeners/PlayerListener.java b/src/main/java/de/fussball/plugin/listeners/PlayerListener.java new file mode 100644 index 0000000..f69d81b --- /dev/null +++ b/src/main/java/de/fussball/plugin/listeners/PlayerListener.java @@ -0,0 +1,89 @@ +package de.fussball.plugin.listeners; + +import de.fussball.plugin.Fussball; +import de.fussball.plugin.game.Game; +import de.fussball.plugin.game.Team; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.player.*; + +public class PlayerListener implements Listener { + + private final Fussball plugin; + public PlayerListener(Fussball plugin) { this.plugin = plugin; } + + /** Spieler disconnected → aus dem Spiel entfernen */ + @EventHandler + public void onQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + Game game = plugin.getGameManager().getPlayerGame(player); + if (game != null) game.removePlayer(player); + // Auch als Zuschauer entfernen + Game spectatorGame = plugin.getGameManager().getSpectatorGame(player); + if (spectatorGame != null) spectatorGame.removeSpectator(player); + // Aus Warteschlangen entfernen + plugin.getGameManager().removeFromAllQueues(player); + } + + /** Spieler stirbt → heilen und zurückteleportieren statt Tod */ + @EventHandler + public void onDamage(EntityDamageEvent event) { + if (!(event.getEntity() instanceof Player player)) return; + Game game = plugin.getGameManager().getPlayerGame(player); + if (game == null) return; + if (player.getHealth() - event.getFinalDamage() <= 0) { + event.setCancelled(true); + player.setHealth(20.0); + Team team = game.getTeam(player); + if (team == Team.RED) player.teleport(game.getArena().getRedSpawn()); + else if (team == Team.BLUE) player.teleport(game.getArena().getBlueSpawn()); + } + } + + /** Hunger deaktivieren */ + @EventHandler + public void onFood(FoodLevelChangeEvent event) { + if (!(event.getEntity() instanceof Player player)) return; + if (plugin.getGameManager().isInAnyGame(player)) event.setCancelled(true); + } + + /** Items droppen verhindern */ + @EventHandler + public void onDrop(PlayerDropItemEvent event) { + if (plugin.getGameManager().isInAnyGame(event.getPlayer())) event.setCancelled(true); + } + + /** Items aufheben verhindern */ + @EventHandler + public void onPickup(EntityPickupItemEvent event) { + if (!(event.getEntity() instanceof Player player)) return; + if (plugin.getGameManager().isInAnyGame(player)) event.setCancelled(true); + } + + /** + * Team-Chat: Nachrichten von Spielern im Spiel werden NUR ans eigene Team gesendet. + * Mit "!" am Anfang können Admins global ins Spiel broadcasten. + * Zuschauer sehen alle Team-Chats (mit Label). + */ + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + Game game = plugin.getGameManager().getPlayerGame(player); + if (game == null) return; + + event.setCancelled(true); + String message = event.getMessage(); + + // Admin-Global-Broadcast + if (message.startsWith("!") && player.hasPermission("fussball.admin")) { + game.broadcastAll("§6[Global] §f" + player.getName() + "§7: " + message.substring(1).trim()); + return; + } + game.sendTeamMessage(player, message); + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/listeners/SignListener.java b/src/main/java/de/fussball/plugin/listeners/SignListener.java new file mode 100644 index 0000000..c5547a5 --- /dev/null +++ b/src/main/java/de/fussball/plugin/listeners/SignListener.java @@ -0,0 +1,216 @@ +package de.fussball.plugin.listeners; + +import de.fussball.plugin.Fussball; +import de.fussball.plugin.arena.Arena; +import de.fussball.plugin.game.Game; +import de.fussball.plugin.utils.MessageUtil; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.Sign; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.entity.Player; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +/** + * Fußball-Join-Schilder + * + * Format beim Beschriften (braucht fussball.admin): + * Zeile 1: [Fussball] + * Zeile 2: + * + * Schilder werden in signs.yml gespeichert und überleben Server-Neustarts. + * Aktualisierung erfolgt automatisch bei Spieler-Join/Leave und Spielstart/-ende. + */ +public class SignListener implements Listener { + + private static final String TAG = "[Fussball]"; + private static final String TAG_FORMATTED = "§8[§e⚽§8]"; + + private final Fussball plugin; + + // Location → ArenaName + private final Map signs = new HashMap<>(); // key = "world;x;y;z" + + private final File signFile; + private FileConfiguration signConfig; + + public SignListener(Fussball plugin) { + this.plugin = plugin; + this.signFile = new File(plugin.getDataFolder(), "signs.yml"); + loadSigns(); + } + + // ── Persistenz ────────────────────────────────────────────────────────── + + private void loadSigns() { + if (!signFile.exists()) { + try { signFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } + } + signConfig = YamlConfiguration.loadConfiguration(signFile); + signs.clear(); + if (signConfig.contains("signs")) { + for (String key : signConfig.getConfigurationSection("signs").getKeys(false)) { + String arenaName = signConfig.getString("signs." + key); + signs.put(key, arenaName); + } + } + plugin.getLogger().info("[Fussball] " + signs.size() + " Schilder geladen."); + } + + private void saveSigns() { + signConfig.set("signs", null); + for (Map.Entry e : signs.entrySet()) { + signConfig.set("signs." + e.getKey(), e.getValue()); + } + try { signConfig.save(signFile); } catch (IOException e) { e.printStackTrace(); } + } + + private String locKey(Location l) { + return l.getWorld().getName() + ";" + l.getBlockX() + ";" + l.getBlockY() + ";" + l.getBlockZ(); + } + + // ── Events ─────────────────────────────────────────────────────────────── + + /** Schild beschriften → Fußball-Schild erstellen */ + @EventHandler + public void onSignChange(SignChangeEvent event) { + String line0 = event.getLine(0); + if (line0 == null || !line0.equalsIgnoreCase(TAG)) return; + + Player player = event.getPlayer(); + if (!player.hasPermission("fussball.admin")) { + player.sendMessage(MessageUtil.error("Keine Berechtigung für Fußball-Schilder!")); + event.setCancelled(true); + return; + } + + String arenaName = event.getLine(1); + if (arenaName == null || arenaName.isEmpty()) { + player.sendMessage(MessageUtil.error("Zeile 2 muss den Arena-Namen enthalten!")); + event.setCancelled(true); + return; + } + + Arena arena = plugin.getArenaManager().getArena(arenaName); + if (arena == null) { + player.sendMessage(MessageUtil.error("Arena §e" + arenaName + " §cnicht gefunden!")); + event.setCancelled(true); + return; + } + + event.setLine(0, TAG_FORMATTED); + event.setLine(1, "§e" + arena.getName()); + event.setLine(2, buildStatusLine(arena)); + event.setLine(3, "§7Klick zum Joinen"); + + String key = locKey(event.getBlock().getLocation()); + signs.put(key, arena.getName()); + saveSigns(); + + player.sendMessage(MessageUtil.success("Fußball-Schild für §e" + arena.getName() + " §aerstellt!")); + } + + /** Rechtsklick → Spieler joinen */ + @EventHandler + public void onInteract(PlayerInteractEvent event) { + if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; + Block block = event.getClickedBlock(); + if (block == null || !(block.getState() instanceof Sign sign)) return; + + String key = locKey(block.getLocation()); + if (!signs.containsKey(key)) return; + + event.setCancelled(true); + Player player = event.getPlayer(); + String arenaName = signs.get(key); + + Arena arena = plugin.getArenaManager().getArena(arenaName); + if (arena == null) { + player.sendMessage(MessageUtil.error("Arena nicht gefunden!")); + signs.remove(key); + saveSigns(); + return; + } + if (!arena.isSetupComplete()) { + player.sendMessage(MessageUtil.error("Diese Arena ist noch nicht fertig eingerichtet!")); + return; + } + if (plugin.getGameManager().isInGame(player)) { + player.sendMessage(MessageUtil.error("Du bist bereits in einem Spiel!")); + return; + } + + plugin.getGameManager().createGame(arena).addPlayer(player); + refreshSignsForArena(arenaName); + } + + /** Schild abbauen → aus Liste entfernen */ + @EventHandler + public void onBreak(BlockBreakEvent event) { + String key = locKey(event.getBlock().getLocation()); + if (signs.containsKey(key)) { + signs.remove(key); + saveSigns(); + event.getPlayer().sendMessage(MessageUtil.info("Fußball-Schild entfernt.")); + } + } + + // ── Öffentliche Methode: von Game.java aufrufen ────────────────────────── + + /** Aktualisiert alle Schilder einer Arena. Wird von Game.java aufgerufen. */ + public void refreshSignsForArena(String arenaName) { + for (Map.Entry entry : signs.entrySet()) { + if (!entry.getValue().equalsIgnoreCase(arenaName)) continue; + Location loc = keyToLocation(entry.getKey()); + if (loc == null) continue; + Block block = loc.getBlock(); + if (!(block.getState() instanceof Sign sign)) continue; + + Arena arena = plugin.getArenaManager().getArena(arenaName); + if (arena == null) continue; + + sign.setLine(0, TAG_FORMATTED); + sign.setLine(1, "§e" + arena.getName()); + sign.setLine(2, buildStatusLine(arena)); + sign.setLine(3, "§7Klick zum Joinen"); + sign.update(); + } + } + + private String buildStatusLine(Arena arena) { + Game game = plugin.getGameManager().getGame(arena.getName()); + if (game == null) return "§a● §70/" + arena.getMaxPlayers(); + int players = game.getAllPlayers().size(); + String dot = switch (game.getState()) { + case WAITING -> "§a●"; + case STARTING -> "§e●"; + case RUNNING, GOAL -> "§c●"; + case HALFTIME, OVERTIME, + PENALTY -> "§6●"; + case ENDING -> "§7●"; + }; + return dot + " §7" + players + "/" + arena.getMaxPlayers(); + } + + private Location keyToLocation(String key) { + try { + String[] p = key.split(";"); + org.bukkit.World world = Bukkit.getWorld(p[0]); + if (world == null) return null; + return new Location(world, Integer.parseInt(p[1]), Integer.parseInt(p[2]), Integer.parseInt(p[3])); + } catch (Exception e) { + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/placeholders/FussballPlaceholders.java b/src/main/java/de/fussball/plugin/placeholders/FussballPlaceholders.java new file mode 100644 index 0000000..6b9d260 --- /dev/null +++ b/src/main/java/de/fussball/plugin/placeholders/FussballPlaceholders.java @@ -0,0 +1,69 @@ +package de.fussball.plugin.placeholders; + +import de.fussball.plugin.Fussball; +import de.fussball.plugin.stats.StatsManager; +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.bukkit.entity.Player; + +/** + * PlaceholderAPI-Erweiterung für das Fußball-Plugin. + * + * Verfügbare Platzhalter: + * %fussball_goals% - Tore des Spielers (gesamt) + * %fussball_kicks% - Schüsse des Spielers (gesamt) + * %fussball_wins% - Siege des Spielers + * %fussball_losses% - Niederlagen des Spielers + * %fussball_draws% - Unentschieden des Spielers + * %fussball_games% - Gespielte Spiele des Spielers + * %fussball_winrate% - Siegquote in Prozent (z.B. "67.5") + * %fussball_ingame% - "true" / "false" – ob Spieler gerade im Spiel ist + * %fussball_arena% - Name der aktuellen Arena (oder "–") + * %fussball_score% - Aktueller Spielstand (z.B. "2 : 1") oder "–" + */ +public class FussballPlaceholders extends PlaceholderExpansion { + + private final Fussball plugin; + + public FussballPlaceholders(Fussball plugin) { + this.plugin = plugin; + } + + @Override + public String getIdentifier() { return "fussball"; } + + @Override + public String getAuthor() { return "M_Viper"; } + + @Override + public String getVersion() { return plugin.getDescription().getVersion(); } + + @Override + public boolean persist() { return true; } + + @Override + public String onPlaceholderRequest(Player player, String identifier) { + if (player == null) return ""; + StatsManager stats = plugin.getStatsManager(); + StatsManager.PlayerStats s = stats.getStats(player.getUniqueId()); + + return switch (identifier.toLowerCase()) { + case "goals" -> String.valueOf(s.goals); + case "kicks" -> String.valueOf(s.kicks); + case "wins" -> String.valueOf(s.wins); + case "losses" -> String.valueOf(s.losses); + case "draws" -> String.valueOf(s.draws); + case "games" -> String.valueOf(s.games); + case "winrate" -> String.format("%.1f", s.getWinRate()); + case "ingame" -> String.valueOf(plugin.getGameManager().isInGame(player)); + case "arena" -> { + var game = plugin.getGameManager().getPlayerGame(player); + yield game != null ? game.getArena().getName() : "–"; + } + case "score" -> { + var game = plugin.getGameManager().getPlayerGame(player); + yield game != null ? game.getRedScore() + " : " + game.getBlueScore() : "–"; + } + default -> null; + }; + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/scoreboard/FussballScoreboard.java b/src/main/java/de/fussball/plugin/scoreboard/FussballScoreboard.java new file mode 100644 index 0000000..bdf1aa1 --- /dev/null +++ b/src/main/java/de/fussball/plugin/scoreboard/FussballScoreboard.java @@ -0,0 +1,137 @@ +package de.fussball.plugin.scoreboard; + +import de.fussball.plugin.game.Game; +import de.fussball.plugin.game.GameState; +import de.fussball.plugin.game.Team; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scoreboard.*; + +import java.util.*; + +public class FussballScoreboard { + + private final Game game; + private final Map boards = new HashMap<>(); + + public FussballScoreboard(Game game) { this.game = game; } + + // ── Scoreboard geben / entfernen ───────────────────────────────────────── + + public void give(Player player) { + Scoreboard board = Bukkit.getScoreboardManager().getNewScoreboard(); + Objective obj = board.registerNewObjective("fussball", "dummy", "§e§l⚽ FUSSBALL"); + obj.setDisplaySlot(DisplaySlot.SIDEBAR); + // Team-Namensschilder registrieren + registerTeamTags(board); + boards.put(player.getUniqueId(), board); + player.setScoreboard(board); + update(player, board); + } + + public void remove(Player player) { + boards.remove(player.getUniqueId()); + player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); + } + + public void updateAll() { + for (UUID uuid : game.getAllPlayers()) { + Player p = Bukkit.getPlayer(uuid); + if (p != null && boards.containsKey(uuid)) { + updateTeamTags(boards.get(uuid)); // Team-Tags aktuell halten + update(p, boards.get(uuid)); + } + } + // Auch Zuschauer-Scoreboards updaten + for (UUID uuid : game.getSpectators()) { + Player p = Bukkit.getPlayer(uuid); + if (p != null && boards.containsKey(uuid)) update(p, boards.get(uuid)); + } + } + + // ── Anzeige ────────────────────────────────────────────────────────────── + + private void update(Player player, Scoreboard board) { + Objective obj = board.getObjective("fussball"); + if (obj == null) return; + for (String entry : board.getEntries()) board.resetScores(entry); + + int l = 14; + set(obj, "§r", l--); + set(obj, "§c▶ ROT §f" + game.getRedScore() + " §7: §f" + game.getBlueScore() + " §9BLAU ◀", l--); + set(obj, "§r§r", l--); + + // Zeit / Status-Zeile + switch (game.getState()) { + case RUNNING, GOAL -> { + int m = game.getTimeLeft() / 60, s = game.getTimeLeft() % 60; + String half = game.isSecondHalf() ? "§72. HZ" : "§71. HZ"; + set(obj, "§e⏱ " + String.format("%02d:%02d", m, s) + " " + half, l--); + } + case HALFTIME -> set(obj, "§6⏸ HALBZEIT", l--); + case OVERTIME -> { + int m = game.getTimeLeft() / 60, s = game.getTimeLeft() % 60; + set(obj, "§e⏱ " + String.format("%02d:%02d", m, s) + " §6VL", l--); + } + case PENALTY -> set(obj, "§c⚽ ELFMETER R" + game.getPenaltyRedGoals() + ":B" + game.getPenaltyBlueGoals(), l--); + case STARTING -> set(obj, "§e⏱ §6Startet gleich...", l--); + default -> set(obj, "§7⏳ Warte auf Spieler...", l--); + } + + set(obj, "§r§r§r", l--); + + // Team + Team team = game.getTeam(player); + if (team != null) { + String c = team == Team.RED ? "§c" : "§9"; + set(obj, "§7Dein Team: " + c + team.getDisplayName(), l--); + } else if (game.isSpectator(player)) { + set(obj, "§7Rolle: §8Zuschauer", l--); + } + + set(obj, "§7Spieler: §f" + game.getAllPlayers().size() + "/" + game.getArena().getMaxPlayers(), l--); + set(obj, "§r§r§r§r", l--); + set(obj, "§7Arena: §e" + game.getArena().getName(), l--); + set(obj, "§r§r§r§r§r", l--); + set(obj, "§6§lFußball-Plugin", l); + } + + // ── Team-Namensschilder (farbige Namen über dem Kopf) ──────────────────── + + /** Registriert die Scoreboard-Teams auf dem übergebenen Board (1x bei give()) */ + private void registerTeamTags(Scoreboard board) { + if (board.getTeam("fb_red") == null) { + org.bukkit.scoreboard.Team red = board.registerNewTeam("fb_red"); + red.setPrefix("§c"); red.setColor(org.bukkit.ChatColor.RED); + } + if (board.getTeam("fb_blue") == null) { + org.bukkit.scoreboard.Team blue = board.registerNewTeam("fb_blue"); + blue.setPrefix("§9"); blue.setColor(org.bukkit.ChatColor.BLUE); + } + } + + /** Aktualisiert die Spieler-Einträge in den Teams (wenn jemand joint/geht) */ + public void updateTeamTags(Scoreboard board) { + org.bukkit.scoreboard.Team red = board.getTeam("fb_red"); + org.bukkit.scoreboard.Team blue = board.getTeam("fb_blue"); + if (red == null || blue == null) return; + + // Alle vorherigen Einträge leeren + for (String e : new HashSet<>(red.getEntries())) red.removeEntry(e); + for (String e : new HashSet<>(blue.getEntries())) blue.removeEntry(e); + + // Neu befüllen + for (UUID uuid : game.getRedTeam()) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) red.addEntry(p.getName()); + } + for (UUID uuid : game.getBlueTeam()) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) blue.addEntry(p.getName()); + } + } + + private void set(Objective obj, String text, int value) { + obj.getScore(text).setScore(value); + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/utils/MessageUtil.java b/src/main/java/de/fussball/plugin/utils/MessageUtil.java new file mode 100644 index 0000000..31d6318 --- /dev/null +++ b/src/main/java/de/fussball/plugin/utils/MessageUtil.java @@ -0,0 +1,31 @@ +package de.fussball.plugin.utils; + +import org.bukkit.ChatColor; + +public class MessageUtil { + + public static final String PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.YELLOW + "⚽" + ChatColor.DARK_GRAY + "] " + ChatColor.GRAY; + public static final String ERROR_PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.RED + "✖" + ChatColor.DARK_GRAY + "] " + ChatColor.RED; + public static final String SUCCESS_PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.GREEN + "✔" + ChatColor.DARK_GRAY + "] " + ChatColor.GREEN; + public static final String WARN_PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.YELLOW + "!" + ChatColor.DARK_GRAY + "] " + ChatColor.YELLOW; + + public static String error(String msg) { + return ERROR_PREFIX + msg; + } + + public static String success(String msg) { + return SUCCESS_PREFIX + msg; + } + + public static String warn(String msg) { + return WARN_PREFIX + msg; + } + + public static String info(String msg) { + return PREFIX + msg; + } + + public static String header(String title) { + return ChatColor.GRAY.toString() + ChatColor.STRIKETHROUGH + "-----------------" + ChatColor.RESET + " " + ChatColor.YELLOW + title + ChatColor.GRAY + " " + ChatColor.STRIKETHROUGH + "-----------------"; + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/utils/Messages.java b/src/main/java/de/fussball/plugin/utils/Messages.java new file mode 100644 index 0000000..112ffa8 --- /dev/null +++ b/src/main/java/de/fussball/plugin/utils/Messages.java @@ -0,0 +1,31 @@ +package de.fussball.plugin.utils; + +import de.fussball.plugin.Fussball; + +/** + * Liest alle Broadcast-Texte aus config.yml. + * Platzhalter: {player}, {team}, {score}, {time}, {reason}, {n} + */ +public class Messages { + + private static Fussball plugin; + + public static void init(Fussball p) { plugin = p; } + + /** Liest eine Nachricht aus config.yml → messages. */ + public static String get(String key) { + if (plugin == null) return "§c[MSG:" + key + "]"; + return plugin.getConfig().getString("messages." + key, "§c[MSG:" + key + "]"); + } + + /** Liest Nachricht und ersetzt Platzhalter: replace("player", "Hans", "team", "Rot", ...) */ + public static String get(String key, String... pairs) { + String msg = get(key); + for (int i = 0; i + 1 < pairs.length; i += 2) { + msg = msg.replace("{" + pairs[i] + "}", pairs[i + 1]); + } + return msg; + } + + public static String prefix() { return get("prefix"); } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..654ba3f --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,119 @@ +# ───────────────────────────────────────────── +# Fußball-Plugin Konfiguration +# ───────────────────────────────────────────── + +defaults: + min-players: 2 + max-players: 10 + game-duration: 300 # Sekunden (5 Min × 2 Halbzeiten) + +ball: + kick-power: 1.1 + sprint-kick-power: 1.8 + kick-vertical: 0.3 + charged-min-power: 1.3 + charged-max-power: 3.8 + +gameplay: + offside-enabled: true # Abseits an/aus + foul-detection-enabled: true # Foul-Erkennung an/aus + freekick-distance: 5.0 # Mindestabstand Gegner beim Freistoß (Blöcke) + freekick-duration: 600 # Ticks bis Freistoß automatisch freigegeben wird + goalkeeper-hold-range: 2.5 # Wie nah der TW am Ball sein muss um ihn zu halten + goalkeeper-throw-power: 1.8 # Wurfstärke des TW + out-of-bounds-tolerance: 2.0 # Toleranz außerhalb Spielfeld (Blöcke) + out-of-bounds-countdown: 5 # Sekunden bis Disqualifikation + +# ── Nachrichten (alle editierbar) ────────────────── +# Verfügbare Platzhalter je nach Kontext: +# {player} = Spielername +# {team} = Teamname +# {score} = Spielstand (z.B. "2 : 1") +# {time} = Minute/Zeit +# {reason} = Grund +# {n} = Zahl + +messages: + prefix: "§8[§e⚽§8] §r" + + # Spielbeginn / Halbzeit / Ende + game-start: "§a§lANPFIFF! ⚽" + game-start-sub: "§7Viel Erfolg!" + second-half: "§a§l▶ 2. HALBZEIT! ◀" + second-half-sub: "§7Die Seiten wurden getauscht!" + halftime: "§e§lHALBZEIT! ⏸" + halftime-score: "§7Spielstand: §c{score}" + halftime-resume: "§e⏱ 2. Halbzeit in §e{n} §6Sek!" + overtime: "§6§lVERLÄNGERUNG! ⚡" + overtime-sub: "§710 Minuten extra!" + penalty-start: "§c§lELFMETERSCHIEßEN! ⚽" + penalty-sub: "§75 Schüsse pro Team" + game-end-win: "§6§lGEWONNEN! 🏆" + game-end-win-sub: "§7Herzlichen Glückwunsch!" + game-end-lose: "§c§lVERLOREN!" + game-end-lose-sub: "§7Gutes Spiel!" + game-end-draw: "§7UNENTSCHIEDEN" + result: "§7Endergebnis: §c{score}" + + # Tor + goal-banner: "§e§l⚽ T - O - R ⚽" + goal-team: "{team} §7hat ein Tor erzielt!" + goal-scorer: "§7Torschütze: §e{player}" + goal-score: "§7Spielstand: §c{score}" + goal-title: "§e⚽ TOR!" + goal-continue: "§aWeiter geht's!" + + # Warn-Nachrichten + time-1min: "§e⏱ Noch §61 Minute§e!" + time-30sec: "§e⏱ Noch §630 Sekunden§e!" + + # Abseits + offside: "§e⚠ §lABSEITS! §7— {player} stand im Abseits!" + offside-title: "§e⚠ ABSEITS!" + offside-sub: "§7Freistoß für {team}" + + # Foul & Karten + foul: "§c⚠ §lFOUL! §7{player} hat gefoult!" + yellow-card: "§e🟨 §lGELBE KARTE §7— {player} §8({reason})" + yellow-card-2: "§e🟨→§c🟥 §lGELB-ROT §7— {player} §8(2. Gelbe Karte)" + red-card: "§c🟥 §lROTE KARTE §7— {player} §8({reason})" + red-card-title: "§c§lROTE KARTE!" + red-card-sub: "§7Du wurdest vom Platz gestellt!" + + # Freistoß + freekick: "§e⚽ §lFREISTOSS §7für {team}!" + freekick-hint: "§7Gegner müssen §e{n} Blöcke §7Abstand halten." + freekick-push: "§cAbstand halten! Mindestens {n} Blöcke vom Ball!" + + # Torwart + goalkeeper-assigned: "§6Du bist §lTorwart§6! Rechtsklick = Ball halten, Rechtsklick wieder = werfen." + goalkeeper-hold: "§6TW §f{player} §7hält den Ball!" + goalkeeper-throw: "§6TW §f{player} §7wirft den Ball!" + goalkeeper-no-hold: "§cDu kannst den Ball nur in deiner eigenen Hälfte halten!" + + # Aus / Einwurf / Ecke / Abstoß + out-side: "§e⚽ §7Ball im Aus! §7Einwurf für {team}§7!" + out-corner: "§e⚽ §7Ball im Aus! §7Ecke für {team}§7!" + out-goal-kick: "§e⚽ §7Ball im Aus! §7Abstoß für {team}§7!" + + # Feldgrenze-Warnung + boundary-warn: "§c⚠ §lSPIELFELDGRENZE! §7Kehre in §e{n} Sek §7zurück!" + boundary-return: "§aWieder im Spielfeld!" + boundary-disq: "§c⚠ §e{player} §cwurde disqualifiziert! (Spielfeldgrenze)" + boundary-disq-self: "§cDu wurdest disqualifiziert, weil du zu lange außerhalb warst!" + + # Spieler beitreten / verlassen + player-join: "§e{player} §7ist beigetreten! §8({n}/{max})" + player-leave: "§e{player} §7hat das Spiel verlassen!" + team-red: "§cRotes Team" + team-blue: "§9Blaues Team" + + # Matchbericht + report-header: "§e§l━━━━━━ MATCHBERICHT ━━━━━━" + report-footer: "§e§l━━━━━━━━━━━━━━━━━━━━━━━━━" + report-goals: "§7Tore:" + report-cards: "§7Karten:" + report-fouls: "§7Fouls:" + report-offside: "§7Abseits:" + report-mvp: "§6⭐ MVP: §e{player} §7({n} Tore)" + report-no-events: "§8Keine Ereignisse." \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..e03c412 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,23 @@ +name: Fussball +version: 2.0.0 +main: de.fussball.plugin.Fussball +api-version: 1.21 +author: M_Viper +description: Ein vollständiges Fußball-Minigame Plugin für Minecraft + +softdepend: + - PlaceholderAPI + +permissions: + fussball.admin: + description: Zugriff auf alle Admin-Befehle + default: op + fussball.play: + description: Spiele spielen + default: true + +commands: + fussball: + description: Hauptbefehl des Fußball-Plugins + usage: /fussball + aliases: [fb, soccer] \ No newline at end of file