From ce1b67754d579ec7ffebc65cb0f8712d4a9d6f08 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sat, 28 Feb 2026 08:38:01 +0100 Subject: [PATCH] Update from Git Manager GUI --- .../java/de/fussball/plugin/Fussball.java | 4 + .../java/de/fussball/plugin/arena/Arena.java | 16 +- .../plugin/commands/FussballCommand.java | 108 +++++- .../java/de/fussball/plugin/game/Game.java | 364 +++++++++++++++++- .../plugin/listeners/BallListener.java | 8 + .../plugin/listeners/PlayerListener.java | 3 + .../plugin/scoreboard/FussballScoreboard.java | 20 +- .../fussball/plugin/stats/MatchHistory.java | 119 ++++++ .../plugin/{ => stats}/StatsManager.java | 7 - src/main/resources/config.yml | 71 +++- src/main/resources/plugin.yml | 2 +- 11 files changed, 660 insertions(+), 62 deletions(-) create mode 100644 src/main/java/de/fussball/plugin/stats/MatchHistory.java rename src/main/java/de/fussball/plugin/{ => stats}/StatsManager.java (96%) diff --git a/src/main/java/de/fussball/plugin/Fussball.java b/src/main/java/de/fussball/plugin/Fussball.java index d0b4f32..a37ef56 100644 --- a/src/main/java/de/fussball/plugin/Fussball.java +++ b/src/main/java/de/fussball/plugin/Fussball.java @@ -7,6 +7,7 @@ import de.fussball.plugin.game.GameManager; import de.fussball.plugin.hologram.HologramManager; import de.fussball.plugin.listeners.*; import de.fussball.plugin.placeholders.FussballPlaceholders; +import de.fussball.plugin.stats.MatchHistory; import de.fussball.plugin.stats.StatsManager; import de.fussball.plugin.utils.Messages; import org.bukkit.configuration.serialization.ConfigurationSerialization; @@ -20,6 +21,7 @@ public class Fussball extends JavaPlugin { private StatsManager statsManager; private SignListener signListener; private HologramManager hologramManager; + private MatchHistory matchHistory; @Override public void onEnable() { @@ -33,6 +35,7 @@ public class Fussball extends JavaPlugin { statsManager = new StatsManager(this); signListener = new SignListener(this); hologramManager = new HologramManager(this); + matchHistory = new MatchHistory(this); Messages.init(this); registerCommands(); @@ -76,4 +79,5 @@ public class Fussball extends JavaPlugin { public StatsManager getStatsManager() { return statsManager; } public SignListener getSignListener() { return signListener; } public HologramManager getHologramManager() { return hologramManager; } + public MatchHistory getMatchHistory() { return matchHistory; } } \ 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 index 18ad071..62fc7d8 100644 --- a/src/main/java/de/fussball/plugin/arena/Arena.java +++ b/src/main/java/de/fussball/plugin/arena/Arena.java @@ -93,20 +93,20 @@ public class Arena implements ConfigurationSerializable { if (Math.abs(dir.getZ()) > Math.abs(dir.getX())) { // Feld läuft entlang Z-Achse if (isRedGoal) { - if (redAxis < blueAxis) maxZ = Math.max(maxZ, maxZ + depth); - else minZ = Math.min(minZ, minZ - depth); + if (redAxis < blueAxis) maxZ += depth; + else minZ -= depth; } else { - if (blueAxis > redAxis) minZ = Math.min(minZ, minZ - depth); - else maxZ = Math.max(maxZ, maxZ + depth); + if (blueAxis > redAxis) minZ -= depth; + else maxZ += depth; } } else { // Feld läuft entlang X-Achse if (isRedGoal) { - if (redAxis < blueAxis) maxX = Math.max(maxX, maxX + depth); - else minX = Math.min(minX, minX - depth); + if (redAxis < blueAxis) maxX += depth; + else minX -= depth; } else { - if (blueAxis > redAxis) minX = Math.min(minX, minX - depth); - else maxX = Math.max(maxX, maxX + depth); + if (blueAxis > redAxis) minX -= depth; + else maxX += depth; } } } else { diff --git a/src/main/java/de/fussball/plugin/commands/FussballCommand.java b/src/main/java/de/fussball/plugin/commands/FussballCommand.java index 2daef7d..b9e850f 100644 --- a/src/main/java/de/fussball/plugin/commands/FussballCommand.java +++ b/src/main/java/de/fussball/plugin/commands/FussballCommand.java @@ -5,8 +5,10 @@ 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.game.Team; import de.fussball.plugin.hologram.FussballHologram; import de.fussball.plugin.hologram.HologramManager; +import de.fussball.plugin.stats.MatchHistory; import de.fussball.plugin.stats.StatsManager; import de.fussball.plugin.utils.MessageUtil; import org.bukkit.Bukkit; @@ -202,6 +204,82 @@ public class FussballCommand implements CommandExecutor, TabCompleter { handleDebug(player, arena); } + // ── Teamwahl ───────────────────────────────────────────────────── + case "team" -> { + if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; } + if (args.length < 2) { + player.sendMessage(MessageUtil.error("Benutze: /fb team rot|blau")); + return true; + } + Game game = plugin.getGameManager().getPlayerGame(player); + if (game == null) { + // Noch nicht im Spiel – Wunsch für nächstes Beitreten speichern + // (Spiel muss noch gesucht werden – Wunsch für alle Spiele gilt nicht; + // Spieler muss zuerst beitreten) + player.sendMessage(MessageUtil.warn("Du bist in keinem Spiel. Tritt zuerst mit /fb join bei.")); + return true; + } + if (game.getState() != GameState.WAITING && game.getState() != GameState.STARTING) { + player.sendMessage(MessageUtil.error("Teamwahl ist nur vor Spielstart möglich!")); + return true; + } + Team desired = switch (args[1].toLowerCase()) { + case "rot", "red", "r" -> Team.RED; + case "blau", "blue", "b" -> Team.BLUE; + default -> null; + }; + if (desired == null) { + player.sendMessage(MessageUtil.error("Ungültiges Team! Benutze: rot oder blau")); + return true; + } + game.requestTeam(player, desired); + } + + // ── Match-History ──────────────────────────────────────────────── + case "history" -> { + int count = 5; + if (args.length >= 2) { try { count = Integer.parseInt(args[1]); } catch (NumberFormatException ignored) {} } + count = Math.max(1, Math.min(count, 20)); + List> matches = plugin.getMatchHistory().getMatches(count); + sender.sendMessage(MessageUtil.header("📋 Match-History (" + matches.size() + " Einträge)")); + if (matches.isEmpty()) { + sender.sendMessage(MessageUtil.warn("Noch keine Spiele gespeichert.")); + return true; + } + for (int i = 0; i < matches.size(); i++) { + Map raw = matches.get(i); + // Sicher auslesen über Object → String/Number-Cast + String date = raw.get("date") instanceof String s ? s : "?"; + String arena = raw.get("arena") instanceof String s ? s : "?"; + String winner = raw.get("winner") instanceof String s ? s : "Unentschieden"; + int rs = raw.get("redScore") instanceof Number n ? n.intValue() : 0; + int bs = raw.get("blueScore") instanceof Number n ? n.intValue() : 0; + int rp = raw.get("redPoss") instanceof Number n ? n.intValue() : 50; + int bp = raw.get("bluePoss") instanceof Number n ? n.intValue() : 50; + int pr = raw.get("penaltyRed") instanceof Number n ? n.intValue() : 0; + int pb = raw.get("penaltyBlue") instanceof Number n ? n.intValue() : 0; + String penStr = (pr + pb > 0) ? " §8(Elfm. §c" + pr + "§8:§9" + pb + "§8)" : ""; + + String winColor = winner.equals("Rot") ? "§c" : winner.equals("Blau") ? "§9" : "§7"; + sender.sendMessage("§e#" + (i+1) + " §8[" + date + "] §7" + arena + + " §c" + rs + "§7:§9" + bs + penStr + + " §8│ " + winColor + winner + + " §8│ §7Besitz §c" + rp + "% §7/ §9" + bp + "%"); + } + } + + // ── Drop Ball (Admin) ───────────────────────────────────────────── + case "dropball" -> { + if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; } + if (args.length < 2) { sender.sendMessage(MessageUtil.error("Benutze: /fb dropball ")); return true; } + Game dropGame = plugin.getGameManager().getGame(args[1]); + if (dropGame == null) { sender.sendMessage(MessageUtil.error("Kein aktives Spiel in §e" + args[1] + "§c!")); return true; } + Location dropLoc = null; + if (sender instanceof Player p) dropLoc = p.getLocation(); + dropGame.dropBall(dropLoc); + sender.sendMessage(MessageUtil.success("Schiedsrichterball ausgeführt!")); + } + // ── Hologramm-Verwaltung ───────────────────────────────────────── // /fb hologram set goals|wins – Hologramm erstellen // /fb hologram remove – Nächstes Hologramm (< 5 Blöcke) entfernen @@ -302,9 +380,21 @@ public class FussballCommand implements CommandExecutor, TabCompleter { case "redpenaltyspot" -> { arena.setRedPenaltySpot(player.getLocation()); player.sendMessage(MessageUtil.success("Roter Elfmeter-Punkt gesetzt: " + locStr(player.getLocation()))); } case "bluepenaltyspot" -> { arena.setBluePenaltySpot(player.getLocation()); player.sendMessage(MessageUtil.success("Blauer Elfmeter-Punkt gesetzt: " + locStr(player.getLocation()))); } case "spectatorspawn" -> { arena.setSpectatorSpawn(player.getLocation()); player.sendMessage(MessageUtil.success("Zuschauer-Spawn 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 "minplayers" -> { + if (args.length < 4) return; + try { arena.setMinPlayers(Integer.parseInt(args[3])); player.sendMessage(MessageUtil.success("Min-Spieler: §e" + args[3])); } + catch (NumberFormatException ex) { player.sendMessage(MessageUtil.error("§e" + args[3] + " §cist keine gültige Zahl!")); } + } + case "maxplayers" -> { + if (args.length < 4) return; + try { arena.setMaxPlayers(Integer.parseInt(args[3])); player.sendMessage(MessageUtil.success("Max-Spieler: §e" + args[3])); } + catch (NumberFormatException ex) { player.sendMessage(MessageUtil.error("§e" + args[3] + " §cist keine gültige Zahl!")); } + } + case "duration" -> { + if (args.length < 4) return; + try { arena.setGameDuration(Integer.parseInt(args[3])); player.sendMessage(MessageUtil.success("Spieldauer: §e" + args[3] + "s")); } + catch (NumberFormatException ex) { player.sendMessage(MessageUtil.error("§e" + args[3] + " §cist keine gültige Zahl!")); } + } case "info" -> { player.sendMessage(MessageUtil.header("Arena: " + arena.getName())); player.sendMessage("§7 Lobby: " + check(arena.getLobby())); @@ -388,11 +478,13 @@ public class FussballCommand implements CommandExecutor, TabCompleter { 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 team rot|blau §7- Teamwahl (vor Spielstart)"); 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"); + s.sendMessage("§e/fb history [n] §7- Letzte Spiele anzeigen"); if (s.hasPermission("fussball.admin")) { - s.sendMessage("§c§lAdmin: §ccreate / delete / setup / stop / debug"); + s.sendMessage("§c§lAdmin: §ccreate / delete / setup / stop / debug / dropball"); s.sendMessage("§c§lAdmin: §chologram set goals|wins / remove / reload"); } } @@ -418,9 +510,9 @@ public class FussballCommand implements CommandExecutor, TabCompleter { 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", "setgk", "debug", "hologram")); - } else if (args.length == 2 && List.of("join","delete","setup","stop","setgk","debug","spectate").contains(args[0].toLowerCase())) { + list.addAll(List.of("join", "leave", "list", "stats", "top", "spectate", "team", "history")); + if (sender.hasPermission("fussball.admin")) list.addAll(List.of("create", "delete", "setup", "stop", "setgk", "debug", "hologram", "dropball")); + } else if (args.length == 2 && List.of("join","delete","setup","stop","setgk","debug","spectate","dropball").contains(args[0].toLowerCase())) { list.addAll(plugin.getArenaManager().getArenaNames()); } else if (args.length == 2 && args[0].equalsIgnoreCase("hologram")) { list.addAll(List.of("set", "remove", "delete", "reload", "list")); @@ -448,6 +540,8 @@ public class FussballCommand implements CommandExecutor, TabCompleter { "minplayers","maxplayers","duration","info")); } else if (args.length == 2 && args[0].equalsIgnoreCase("top")) { list.addAll(List.of("goals", "wins")); + } else if (args.length == 2 && args[0].equalsIgnoreCase("team")) { + list.addAll(List.of("rot", "blau")); } String input = args[args.length - 1].toLowerCase(); list.removeIf(s -> !s.toLowerCase().startsWith(input)); diff --git a/src/main/java/de/fussball/plugin/game/Game.java b/src/main/java/de/fussball/plugin/game/Game.java index 5d24abf..5cfdad8 100644 --- a/src/main/java/de/fussball/plugin/game/Game.java +++ b/src/main/java/de/fussball/plugin/game/Game.java @@ -3,6 +3,7 @@ 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.MatchHistory; import de.fussball.plugin.stats.StatsManager; import de.fussball.plugin.utils.MessageUtil; import de.fussball.plugin.utils.Messages; @@ -47,6 +48,23 @@ public class Game { // ── Statistik ────────────────────────────────────────────────────────── private final Map goals = new HashMap<>(); private final Map kicks = new HashMap<>(); + + // ── Ballbesitz-Tracking ──────────────────────────────────────────────── + /** Ticks, die Rot in Ballbesitz war (letzter Berührungs-Team = Rot) */ + private long redPossessionTicks = 0; + /** Ticks, die Blau in Ballbesitz war */ + private long bluePossessionTicks = 0; + /** Gesamtticks mit bekanntem Besitz */ + private long totalPossessionTicks = 0; + + // ── Teamwahl durch Spieler ───────────────────────────────────────────── + /** Spieler → gewünschtes Team (gesetzt per /fb team red|blue vor Spielstart) */ + private final Map requestedTeam = new HashMap<>(); + + // ── Pass-Tracking ────────────────────────────────────────────────────── + /** Position des Balls beim letzten Schuss – für Kurz-/Langpass-Erkennung */ + private Location lastKickLocation = null; + private static final double LONG_PASS_DISTANCE = 20.0; // ── FEHLENDE VARIABLE ──────────────────────────────────────────────── private final Map outOfBoundsCountdown = new HashMap<>(); @@ -76,6 +94,8 @@ public class Game { private int spawnCooldown = 0; private int ballMissingTicks = 0; // Ticks ohne lebenden Ball → Respawn private static final int BALL_MISSING_TIMEOUT = 80; // 4s + /** Ticks, in denen der Anstoß-Kreis erzwungen wird (Gegner müssen Abstand halten) */ + private int kickoffEnforceTicks = 0; // ── Torwart ──────────────────────────────────────────────────────────── private UUID redGoalkeeper = null; @@ -204,17 +224,48 @@ public class Game { // ── Team-Zuweisung ─────────────────────────────────────────────────────── - /** Auto-Balance: immer ins kleinere Team */ + /** Auto-Balance: immer ins kleinere Team, sofern keine Teamwahl vorliegt */ private void assignTeam(Player player) { - if (redTeam.size() <= blueTeam.size()) { + Team preferred = requestedTeam.remove(player.getUniqueId()); + Team assigned; + + if (preferred != null) { + // Gewünschtes Team: nur wenn es nicht deutlich größer ist (max 1 mehr) + int redSize = redTeam.size(); + int blueSize = blueTeam.size(); + boolean canJoinPreferred = (preferred == Team.RED && redSize <= blueSize + 1) + || (preferred == Team.BLUE && blueSize <= redSize + 1); + if (canJoinPreferred) { + assigned = preferred; + player.sendMessage("§a⚽ Du spielst wie gewünscht im " + preferred.getColorCode() + preferred.getDisplayName() + "§a Team!"); + } else { + assigned = (redTeam.size() <= blueTeam.size()) ? Team.RED : Team.BLUE; + player.sendMessage(MessageUtil.warn("Dein Wunsch-Team war zu groß – du wurdest automatisch zugeteilt.")); + } + } else { + assigned = (redTeam.size() <= blueTeam.size()) ? Team.RED : Team.BLUE; + } + + if (assigned == Team.RED) { redTeam.add(player.getUniqueId()); - player.sendMessage(MessageUtil.success("Du bist im §cRoten Team§a!")); + if (preferred == null) 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!")); + if (preferred == null) player.sendMessage(MessageUtil.success("Du bist im §9Blauen Team§a!")); } } + /** + * Setzt das Wunsch-Team für den nächsten Spielbeitritt. + * Nur im WAITING/STARTING-Zustand sinnvoll. + */ + public void requestTeam(Player player, Team team) { + requestedTeam.put(player.getUniqueId(), team); + String c = team == Team.RED ? "§c" : "§9"; + player.sendMessage(MessageUtil.info("Teamwunsch gesetzt: " + c + team.getDisplayName() + + "§7. Wird beim nächsten Spielstart berücksichtigt (wenn möglich).")); + } + // ── Spieler vorbereiten / zurücksetzen ─────────────────────────────────── private void preparePlayer(Player player) { @@ -231,17 +282,31 @@ public class Game { private void applyTeamColors(Player player, Team team) { org.bukkit.Color color = team == Team.RED ? org.bukkit.Color.RED : org.bukkit.Color.BLUE; + String teamCode = team == Team.RED ? "§c" : "§9"; + + // Trikot-Nummer: Position im Team-Array (1-basiert) + List teamList = team == Team.RED ? redTeam : blueTeam; + int jerseyNum = teamList.indexOf(player.getUniqueId()) + 1; + String numStr = jerseyNum > 0 ? " §f#" + jerseyNum : ""; + 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); + meta.setColor(color); + // Trikot-Nummer auf dem Brustpanzer anzeigen + if (item.getType() == Material.LEATHER_CHESTPLATE) { + meta.setDisplayName(teamCode + team.getDisplayName() + numStr); + } + item.setItemMeta(meta); } } - player.getInventory().setHelmet(armor[0]); player.getInventory().setChestplate(armor[1]); - player.getInventory().setLeggings(armor[2]); player.getInventory().setBoots(armor[3]); + player.getInventory().setHelmet(armor[0]); + player.getInventory().setChestplate(armor[1]); + player.getInventory().setLeggings(armor[2]); + player.getInventory().setBoots(armor[3]); } /** Setzt einen Spieler oder Zuschauer zurück (Lobby, ADVENTURE, Inventar leer) */ @@ -619,11 +684,15 @@ public class Game { int itLeft = injuryTimeBuffer; if (!inInjuryTime) { inInjuryTime = true; - int mins = (int) Math.ceil((itLeft + 1) / 60.0); - broadcastAll(Messages.get("injury-time", "n", String.valueOf(mins))); + // BUG FIX: tatsächliche Sekunden anzeigen statt gerundete Minuten + int totalSec = itLeft + 1; + String injDisplay = totalSec >= 60 + ? String.format("+%d:%02d", totalSec / 60, totalSec % 60) + : "+" + totalSec + "s"; + broadcastAll(Messages.get("injury-time", "n", injDisplay)); for (UUID uuid : allPlayers) { Player p = Bukkit.getPlayer(uuid); - if (p != null) p.sendTitle("§c⏱", "§8+" + (itLeft + 1) + "s Nachspielzeit", 5, 30, 5); + if (p != null) p.sendTitle("§c⏱", "§8" + injDisplay + " Nachspielzeit", 5, 30, 5); } } scoreboard.updateAll(); updateBossBar(); return; @@ -641,6 +710,12 @@ public class Game { checkHeaderOpportunities(); checkAfkPlayers(); + // Anstoß-Kreis: Gegner fernhalten wenn Anstoß läuft + if (kickoffEnforceTicks > 0) { + kickoffEnforceTicks--; + enforceKickoffCircle(); + } + // Freistoß-Abstandsdurchsetzung if (freekickLocation != null) { freekickTicks--; @@ -662,11 +737,14 @@ public class Game { injuryTimeBuffer--; if (!inInjuryTime) { inInjuryTime = true; - int mins = (int) Math.ceil((injuryTimeBuffer + 1) / 60.0); - broadcastAll(Messages.get("injury-time", "n", String.valueOf(mins))); + int totalSec = injuryTimeBuffer + 1; + String injDisplay = totalSec >= 60 + ? String.format("+%d:%02d", totalSec / 60, totalSec % 60) + : "+" + totalSec + "s"; + broadcastAll(Messages.get("injury-time", "n", injDisplay)); for (UUID uuid : allPlayers) { Player p = Bukkit.getPlayer(uuid); - if (p != null) p.sendTitle("§c+"+mins+"'", "§8Nachspielzeit!", 5, 30, 5); + if (p != null) p.sendTitle("§c" + injDisplay, "§8Nachspielzeit!", 5, 30, 5); } } scoreboard.updateAll(); updateBossBar(); return; @@ -710,6 +788,15 @@ public class Game { } ballMissingTicks = 0; + // ── Ballbesitz-Tracking ────────────────────────────────────── + if (state == GameState.RUNNING || state == GameState.OVERTIME) { + if (lastTouchTeam != null) { + totalPossessionTicks++; + if (lastTouchTeam == Team.RED) redPossessionTicks++; + else bluePossessionTicks++; + } + } + // ── Torwart: Ball-Position aktualisieren ───────────────────── if (ball.isHeld()) { ball.updateHeldPosition(); @@ -877,6 +964,24 @@ public class Game { if (ball.getDistanceTo(p) >= 2.2) continue; if (throwInTeam != null && getTeam(p) != throwInTeam) continue; + // ── Handball-Erkennung ──────────────────────────────────────────── + // Ein kauernder Spieler, der den Ball berührt, gilt als Handspiel. + // (Kauern = Arme seitlich gestreckt in Minecraft-Interpretation) + if (plugin.getConfig().getBoolean("gameplay.handball-enabled", true) && p.isSneaking()) { + Location ballLoc = ball.getEntity() != null ? ball.getEntity().getLocation() : null; + if (ballLoc != null) { + double relHeight = ballLoc.getY() - p.getLocation().getY(); + // Ball auf "Arm-Höhe" (0.5 – 1.5 Blöcke) + Spieler kauert = Handball + if (relHeight >= 0.5 && relHeight <= 1.5) { + Team victimTeam = getTeam(p).getOpponent(); + boolean inPenalty = (getTeam(p) == Team.RED && arena.isInRedPenaltyArea(p.getLocation())) + || (getTeam(p) == Team.BLUE && arena.isInBluePenaltyArea(p.getLocation())); + handleHandball(p, victimTeam, p.getLocation(), inPenalty); + continue; + } + } + } + // ── Rückpass-Regel für Torwarte ────────────────────────────────── // Torwart im Auto-Kick: falls Ball vom Mitspieler per Fuß zugespielt → überspringen if (isGoalkeeper(p) && isInOwnHalf(p)) { @@ -993,6 +1098,9 @@ public class Game { + (assistName != null ? " §8(Vorlage: §e" + assistName + "§8)" : "") + " §8[" + color + scoringTeam.getDisplayName() + "§8] §7" + redScore + ":" + blueScore); + // ── Stadionatmosphäre: Torjubel ────────────────────────────────────── + playStadiumGoalAtmosphere(scoringTeam, ownGoal); + final Team concedingTeam = scoringTeam.getOpponent(); // für Anstoß new BukkitRunnable() { public void run() { @@ -1002,6 +1110,8 @@ public class Game { spawnBallDelayed(arena.getBallSpawn()); // ── Anstoß-Team: das Team, das den Treffer kassiert hat ── throwInTeam = concedingTeam; + // ── Anstoß-Kreis für 10 Sekunden erzwingen ────────────── + kickoffEnforceTicks = 200; // 10 Sekunden broadcastAll(Messages.get("kickoff-team", "team", concedingTeam == Team.RED ? "§cRotes Team" : "§9Blaues Team")); @@ -1038,8 +1148,13 @@ public class Game { if (bossBar == null) return; String timeStr; if (inInjuryTime) { - int injMins = (int) Math.ceil(injuryTimeBuffer / 60.0); - timeStr = "§c+" + injMins + "' "; + // BUG FIX: zeige tatsächliche Restzeit in Sekunden statt gerundete Minuten + int injSec = injuryTimeBuffer; + if (injSec >= 60) { + timeStr = "§c+" + String.format("%d:%02d", injSec / 60, injSec % 60); + } else { + timeStr = "§c+" + injSec + "s"; + } } else { int m = timeLeft / 60, s = timeLeft % 60; timeStr = String.format("%02d:%02d", m, s); @@ -1504,10 +1619,7 @@ public class Game { if (insideInner) { // ── Zu nah / auf dem Spielfeld → raus schieben ─────────────── - // Nächsten Punkt auf dem inneren Rand berechnen und leicht weiter raus - double pushX = Math.max(innerMinX, Math.min(px, innerMaxX)); - double pushZ = Math.max(innerMinZ, Math.min(pz, innerMaxZ)); - // Richtung nach außen bestimmen + // Richtung nach außen bestimmen (kürzester Weg über alle 4 Seiten) double dxMin = Math.abs(px - innerMinX), dxMax = Math.abs(px - innerMaxX); double dzMin = Math.abs(pz - innerMinZ), dzMax = Math.abs(pz - innerMaxZ); double minDist = Math.min(Math.min(dxMin, dxMax), Math.min(dzMin, dzMax)); @@ -1649,6 +1761,13 @@ public class Game { broadcastAll(Messages.get("report-header")); broadcastAll("§7Endergebnis: §c" + redScore + " §7: §9" + blueScore); + // Ballbesitz + int redPoss = getRedPossessionPercent(); + int bluePoss = getBluePossessionPercent(); + if (redPoss >= 0) { + broadcastAll("§7Ballbesitz: §c" + redPoss + "% §7(Rot) vs §9" + bluePoss + "% §7(Blau)"); + } + 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()); @@ -1713,6 +1832,21 @@ public class Game { // Matchbericht senden sendMatchReport(); + // ── Match-History speichern ────────────────────────────────────────── + { + int redPoss = getRedPossessionPercent(); + int bluePoss = getBluePossessionPercent(); + List goalEvts = matchEvents.stream() + .filter(e -> e.contains("TOR:")).collect(Collectors.toList()); + String winnerName = winner != null ? winner.getDisplayName() : null; + MatchHistory.MatchRecord record = new MatchHistory.MatchRecord( + arena.getName(), redScore, blueScore, winnerName, + secondsPlayed, redPoss < 0 ? 50 : redPoss, bluePoss < 0 ? 50 : bluePoss, + penaltyRedGoals, penaltyBlueGoals, goalEvts + ); + plugin.getMatchHistory().saveMatch(record); + } + // Ergebnis-Nachrichten broadcastAll("§e§l╔══════════════════════╗"); if (winner == null) { @@ -1723,7 +1857,7 @@ public class Game { } broadcastAll("§e§l╚══════════════════════╝"); broadcastAll("§7Endergebnis: §c" + redScore + " §7: §9" + blueScore); - if (state == GameState.ENDING && penaltyRedGoals + penaltyBlueGoals > 0) { + if (penaltyRedGoals + penaltyBlueGoals > 0) { broadcastAll("§7Elfmeter: §c" + penaltyRedGoals + " §7: §9" + penaltyBlueGoals); } @@ -1880,8 +2014,33 @@ public class Game { if (lastKicker != null && !lastKicker.equals(uuid)) { secondLastKicker = lastKicker; } + + // ── Kurz-/Langpass-Erkennung ───────────────────────────────────────── + if (lastKicker != null && !lastKicker.equals(uuid) && ball != null && ball.getEntity() != null + && lastKickLocation != null && (state == GameState.RUNNING || state == GameState.OVERTIME)) { + Player prevKicker = Bukkit.getPlayer(lastKicker); + Player newKicker = Bukkit.getPlayer(uuid); + if (prevKicker != null && newKicker != null) { + double dist = lastKickLocation.distance(ball.getEntity().getLocation()); + if (dist >= LONG_PASS_DISTANCE && getTeam(prevKicker) == getTeam(newKicker)) { + // Langer Pass innerhalb des Teams + String msg = "§7⚽ §eLangpass §7von §f" + prevKicker.getName() + + " §7zu §f" + newKicker.getName() + + " §8(" + String.format("%.0f", dist) + " Blöcke)"; + for (UUID u : getAllAndSpectators()) { + Player pl = Bukkit.getPlayer(u); + if (pl != null) pl.spigot().sendMessage( + net.md_5.bungee.api.ChatMessageType.ACTION_BAR, + net.md_5.bungee.api.chat.TextComponent.fromLegacyText(msg)); + } + } + } + } + // Aktuelle Ball-Position für nächsten Pass merken + if (ball != null && ball.getEntity() != null) lastKickLocation = ball.getEntity().getLocation().clone(); + this.lastKicker = uuid; - lastKickWasHeader = false; // normaler Schuss / Berührung + lastKickWasHeader = false; Player p = Bukkit.getPlayer(uuid); if (p != null) lastTouchTeam = getTeam(p); if (p != null) kicks.merge(uuid, 1, Integer::sum); @@ -1947,5 +2106,168 @@ public class Game { freekickLocation = null; freekickTicks = 0; offsideCooldown = false; + kickoffEnforceTicks = 0; // Anstoß-Kreis sofort freigeben + } + + // ════════════════════════════════════════════════════════════════════════ + // ANSTOSSKRANZ-DURCHSETZUNG + // ════════════════════════════════════════════════════════════════════════ + + /** + * Während des Anstoßes müssen Gegner einen Mindestabstand zum Mittelpunkt einhalten. + * Radius konfigurierbar über gameplay.kickoff-circle-radius (default: 9.15 Blöcke). + */ + private void enforceKickoffCircle() { + if (throwInTeam == null || arena.getCenter() == null) return; + double radius = plugin.getConfig().getDouble("gameplay.kickoff-circle-radius", 9.15); + double radiusSq = radius * radius; + 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(arena.getCenter().getWorld())) continue; + // Nur X/Z-Abstand (2D-Kreis) + double dx = p.getLocation().getX() - arena.getCenter().getX(); + double dz = p.getLocation().getZ() - arena.getCenter().getZ(); + double distSq = dx * dx + dz * dz; + if (distSq < radiusSq) { + // Spieler nach außen schieben + double dist = Math.sqrt(distSq); + if (dist < 0.01) { dx = 1; dz = 0; dist = 1; } + double factor = (radius - dist) / dist * 0.5; + p.setVelocity(new org.bukkit.util.Vector(dx * factor, 0.1, dz * factor)); + p.spigot().sendMessage(net.md_5.bungee.api.ChatMessageType.ACTION_BAR, + net.md_5.bungee.api.chat.TextComponent.fromLegacyText( + "§cAbstand halten! §7Anstoß-Kreis (" + String.format("%.0f", radius) + "m)")); + } + } + } + + // ════════════════════════════════════════════════════════════════════════ + // HANDBALL-REGEL + // ════════════════════════════════════════════════════════════════════════ + + private void handleHandball(Player fouler, Team freekickForTeam, Location loc, boolean isPenaltyArea) { + if (state != GameState.RUNNING && state != GameState.OVERTIME) return; + broadcastAll("§e✋ §cHANDSPIEL §7von §f" + fouler.getName() + "§7!"); + fouler.sendTitle("§c✋ HANDSPIEL!", "§7Freistoß für " + freekickForTeam.getColorCode() + + freekickForTeam.getDisplayName(), 5, 50, 10); + for (UUID uuid : getAllAndSpectators()) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1.2f); + } + logMatchEvent("§e✋ Handball: §e" + fouler.getName()); + addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-foul", 5)); + + if (isPenaltyArea) { + broadcastAll("§c⚠ ELFMETER! §7Handspiel im Strafraum!"); + for (UUID uuid : getAllAndSpectators()) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§7Handspiel im Strafraum!", 5, 50, 10); + } + startFreekick(freekickForTeam, arena.getBallSpawn(), "Elfmeter (Handball)"); + } else { + startFreekick(freekickForTeam, loc, "Handspiel"); + } + } + + // ════════════════════════════════════════════════════════════════════════ + // DROP BALL (Schiedsrichterball) + // ════════════════════════════════════════════════════════════════════════ + + /** + * Schiedsrichterball: Ball wird neutral an einer bestimmten Position gespawnt. + * Beide Teams dürfen sofort spielen (throwInTeam = null). + * Wird bei neutralen Unterbrechungen verwendet. + */ + public void dropBall(Location location) { + if (state != GameState.RUNNING && state != GameState.OVERTIME) return; + if (ball != null) ball.remove(); + throwInTeam = null; + freekickLocation = null; + freekickTicks = 0; + spawnBallDelayed(location != null ? location : arena.getBallSpawn()); + broadcastAll("§8[Schiri] §7⬇ §eSchiedsrichterball §7– beide Teams dürfen spielen!"); + for (UUID uuid : getAllAndSpectators()) { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { + p.sendTitle("§e⬇ DROPBALL", "§7Beide Teams – los!", 5, 30, 5); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.5f, 0.8f); + } + } + logMatchEvent("§8Schiedsrichterball"); + addInjuryTime(5); + } + + // ════════════════════════════════════════════════════════════════════════ + // STADIONATMOSPHÄRE + // ════════════════════════════════════════════════════════════════════════ + + private void playStadiumGoalAtmosphere(Team scoringTeam, boolean ownGoal) { + if (!plugin.getConfig().getBoolean("atmosphere.enabled", true)) return; + Location center = arena.getCenter() != null ? arena.getCenter() : arena.getBallSpawn(); + if (center == null) return; + + // Mehrere Feuerwerke für Jubel-Feeling + int fwCount = plugin.getConfig().getInt("atmosphere.goal-fireworks", 5); + for (int i = 0; i < fwCount; i++) { + final int delay = i * 12; + new BukkitRunnable() { + public void run() { + if (state == GameState.ENDING || state == GameState.WAITING) return; + Location offset = center.clone().add( + (Math.random() - 0.5) * 10, 3 + Math.random() * 4, (Math.random() - 0.5) * 10); + spawnFirework(offset, ownGoal ? scoringTeam.getOpponent() : scoringTeam); + } + }.runTaskLater(plugin, delay); + } + + // Jubel-Sounds für das siegende Team; Stille für das andere + for (UUID uuid : getAllAndSpectators()) { + Player p = Bukkit.getPlayer(uuid); if (p == null) continue; + Team t = getTeam(p); + if (t == scoringTeam || t == null) { + // Jubel-Sound + p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 0.9f); + new BukkitRunnable() { public void run() { + if (p.isOnline()) p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1f, 1.2f); + }}.runTaskLater(plugin, 20L); + } else { + // Enttäuschungs-Sound + p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 0.7f, 0.5f); + } + } + + // Boden-Erschütterung durch Partikel + for (UUID uuid : getAllAndSpectators()) { + Player p = Bukkit.getPlayer(uuid); if (p == null) continue; + p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 2f, 0.5f); + } + } + + /** Spielt einen "Beinahe-Tor"-Sound (z. B. bei Abpraller am Pfosten) */ + public void playNearMissAtmosphere() { + if (!plugin.getConfig().getBoolean("atmosphere.enabled", true)) return; + for (UUID uuid : getAllAndSpectators()) { + Player p = Bukkit.getPlayer(uuid); if (p == null) continue; + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 0.7f); + } + } + + // ════════════════════════════════════════════════════════════════════════ + // BALLBESITZ-GETTER + // ════════════════════════════════════════════════════════════════════════ + + /** Ballbesitz Rot in Prozent (0–100), -1 wenn noch keine Daten */ + public int getRedPossessionPercent() { + if (totalPossessionTicks == 0) return -1; + return (int) Math.round((double) redPossessionTicks / totalPossessionTicks * 100.0); + } + + /** Ballbesitz Blau in Prozent (0–100), -1 wenn noch keine Daten */ + public int getBluePossessionPercent() { + if (totalPossessionTicks == 0) return -1; + return (int) Math.round((double) bluePossessionTicks / totalPossessionTicks * 100.0); } } \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/listeners/BallListener.java b/src/main/java/de/fussball/plugin/listeners/BallListener.java index 800a276..5627714 100644 --- a/src/main/java/de/fussball/plugin/listeners/BallListener.java +++ b/src/main/java/de/fussball/plugin/listeners/BallListener.java @@ -232,6 +232,14 @@ public class BallListener implements Listener { TextComponent.fromLegacyText("§e⚽ Schuss-Power: " + bar + " §f" + (int)(power*100) + "%")); if (power >= 1.0) { chargeMap.remove(player.getUniqueId()); + // throwInTeam-Check: auch beim Auto-Feuer einhalten + if (game.getThrowInTeam() != null && game.getThrowInTeam() != game.getTeam(player)) { + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, + TextComponent.fromLegacyText("§cDu bist nicht dran!")); + cancel(); + return; + } + game.clearThrowIn(); game.setLastKicker(player.getUniqueId()); ball.chargedKick(player, 1.0); cancel(); diff --git a/src/main/java/de/fussball/plugin/listeners/PlayerListener.java b/src/main/java/de/fussball/plugin/listeners/PlayerListener.java index d8751ce..d56d521 100644 --- a/src/main/java/de/fussball/plugin/listeners/PlayerListener.java +++ b/src/main/java/de/fussball/plugin/listeners/PlayerListener.java @@ -119,6 +119,9 @@ public class PlayerListener implements Listener { * Da AsyncPlayerChatEvent asynchron läuft, wird die eigentliche Team-Nachricht * per runTask auf den Haupt-Thread verlagert. */ + @SuppressWarnings("deprecation") // AsyncPlayerChatEvent ist in Paper 1.19+ deprecated. + // Für reines Spigot-Kompatibilitäts-Targeting wird es hier bewusst weiterverwendet. + // Migration zu io.papermc.paper.event.player.AsyncChatEvent möglich sobald Spigot-Support entfällt. @EventHandler(priority = EventPriority.LOWEST) public void onChat(AsyncPlayerChatEvent event) { Player player = event.getPlayer(); diff --git a/src/main/java/de/fussball/plugin/scoreboard/FussballScoreboard.java b/src/main/java/de/fussball/plugin/scoreboard/FussballScoreboard.java index 598eb59..81bd425 100644 --- a/src/main/java/de/fussball/plugin/scoreboard/FussballScoreboard.java +++ b/src/main/java/de/fussball/plugin/scoreboard/FussballScoreboard.java @@ -66,8 +66,11 @@ public class FussballScoreboard { case RUNNING, GOAL -> { String half = game.isSecondHalf() ? "§72. HZ" : "§71. HZ"; if (game.isInInjuryTime()) { - int injMins = (int) Math.ceil(game.getInjuryTimeBuffer() / 60.0); - set(obj, "§c⏱ +" + injMins + "' §7Nachsp. " + half, l--); + int injSec = game.getInjuryTimeBuffer(); + String injStr = injSec >= 60 + ? String.format("+%d:%02d", injSec/60, injSec%60) + : "+" + injSec + "s"; + set(obj, "§c⏱ §f" + injStr + " §7Nachsp. " + half, l--); } else { int m = game.getTimeLeft() / 60, s = game.getTimeLeft() % 60; set(obj, "§e⏱ " + String.format("%02d:%02d", m, s) + " " + half, l--); @@ -76,8 +79,11 @@ public class FussballScoreboard { case HALFTIME -> set(obj, "§6⏸ HALBZEIT", l--); case OVERTIME -> { if (game.isInInjuryTime()) { - int injMins = (int) Math.ceil(game.getInjuryTimeBuffer() / 60.0); - set(obj, "§c⏱ +" + injMins + "' §6VL Nachsp.", l--); + int injSec = game.getInjuryTimeBuffer(); + String injStr = injSec >= 60 + ? String.format("+%d:%02d", injSec/60, injSec%60) + : "+" + injSec + "s"; + set(obj, "§c⏱ §f" + injStr + " §6VL Nachsp.", l--); } else { int m = game.getTimeLeft() / 60, s = game.getTimeLeft() % 60; set(obj, "§e⏱ " + String.format("%02d:%02d", m, s) + " §6VL", l--); @@ -101,6 +107,12 @@ public class FussballScoreboard { set(obj, "§7Spieler: §f" + game.getAllPlayers().size() + "/" + game.getArena().getMaxPlayers(), l--); set(obj, "§r§r§r§r", l--); + + // Ballbesitz anzeigen wenn Daten vorhanden + int redPoss = game.getRedPossessionPercent(); + if (redPoss >= 0) { + set(obj, "§cR " + redPoss + "% §7│ §9" + game.getBluePossessionPercent() + "% B", l--); + } set(obj, "§7Arena: §e" + game.getArena().getName(), l--); set(obj, "§r§r§r§r§r", l--); set(obj, "§6§lFußball-Plugin", l); diff --git a/src/main/java/de/fussball/plugin/stats/MatchHistory.java b/src/main/java/de/fussball/plugin/stats/MatchHistory.java new file mode 100644 index 0000000..a3af2e9 --- /dev/null +++ b/src/main/java/de/fussball/plugin/stats/MatchHistory.java @@ -0,0 +1,119 @@ +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.text.SimpleDateFormat; +import java.util.*; + +/** + * Speichert die letzten 50 Spielergebnisse dauerhaft in matchhistory.yml. + * Wird am Spielende automatisch befüllt und kann per /fb history abgerufen werden. + */ +public class MatchHistory { + + private static final int MAX_ENTRIES = 50; + private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("dd.MM.yyyy HH:mm"); + + private final Fussball plugin; + private final File historyFile; + private FileConfiguration config; + + public MatchHistory(Fussball plugin) { + this.plugin = plugin; + this.historyFile = new File(plugin.getDataFolder(), "matchhistory.yml"); + load(); + } + + // ── Laden / Speichern ──────────────────────────────────────────────────── + + private void load() { + if (!historyFile.exists()) { + try { historyFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } + } + config = YamlConfiguration.loadConfiguration(historyFile); + } + + /** + * Speichert ein Spielergebnis an Stelle 0 (neueste zuerst). + * Alte Einträge über MAX_ENTRIES werden verworfen. + */ + public void saveMatch(MatchRecord record) { + List> matches = new ArrayList<>( + config.contains("matches") ? config.getMapList("matches") : new ArrayList<>()); + + Map entry = new LinkedHashMap<>(); + entry.put("arena", record.arena); + entry.put("date", record.date); + entry.put("redScore", record.redScore); + entry.put("blueScore", record.blueScore); + entry.put("winner", record.winner != null ? record.winner : "Unentschieden"); + entry.put("duration", record.durationSeconds); + entry.put("redPoss", record.redPossession); + entry.put("bluePoss", record.bluePossession); + entry.put("penaltyRed", record.penaltyRed); + entry.put("penaltyBlue", record.penaltyBlue); + if (!record.goalEvents.isEmpty()) entry.put("goals", record.goalEvents); + + matches.add(0, entry); + if (matches.size() > MAX_ENTRIES) matches = matches.subList(0, MAX_ENTRIES); + + config.set("matches", matches); + try { + config.save(historyFile); + } catch (IOException e) { + plugin.getLogger().severe("[MatchHistory] Konnte matchhistory.yml nicht speichern: " + e.getMessage()); + } + } + + /** Gibt die letzten n Spiele zurück (neueste zuerst). */ + public List> getMatches(int limit) { + if (!config.contains("matches")) return Collections.emptyList(); + List> all = config.getMapList("matches"); + return all.subList(0, Math.min(limit, all.size())); + } + + /** Gibt alle gespeicherten Spiele zurück. */ + public List> getMatches() { return getMatches(MAX_ENTRIES); } + + // ── Datenklasse ────────────────────────────────────────────────────────── + + public static class MatchRecord { + public final String arena; + public final String date; + public final int redScore, blueScore; + /** "Rot", "Blau" oder null für Unentschieden */ + public final String winner; + public final int durationSeconds; + /** Ballbesitz-Prozent (Rot) */ + public final int redPossession; + /** Ballbesitz-Prozent (Blau) */ + public final int bluePossession; + public final int penaltyRed, penaltyBlue; + /** Tor-Events aus dem Matchbericht (z. B. "12' Hans [Rot]") */ + public final List goalEvents; + + public MatchRecord(String arena, + int redScore, int blueScore, + String winner, + int durationSeconds, + int redPossession, int bluePossession, + int penaltyRed, int penaltyBlue, + List goalEvents) { + this.arena = arena; + this.date = DATE_FMT.format(new Date()); + this.redScore = redScore; + this.blueScore = blueScore; + this.winner = winner; + this.durationSeconds = durationSeconds; + this.redPossession = redPossession; + this.bluePossession = bluePossession; + this.penaltyRed = penaltyRed; + this.penaltyBlue = penaltyBlue; + this.goalEvents = goalEvents != null ? goalEvents : Collections.emptyList(); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/fussball/plugin/StatsManager.java b/src/main/java/de/fussball/plugin/stats/StatsManager.java similarity index 96% rename from src/main/java/de/fussball/plugin/StatsManager.java rename to src/main/java/de/fussball/plugin/stats/StatsManager.java index c678352..e55f8df 100644 --- a/src/main/java/de/fussball/plugin/StatsManager.java +++ b/src/main/java/de/fussball/plugin/stats/StatsManager.java @@ -103,13 +103,6 @@ public class StatsManager { 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; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 4579c5a..3a92440 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -35,6 +35,11 @@ gameplay: header-power: 1.3 # Schussstärke eines Kopfballs header-cooldown: 10 # Ticks Abklingzeit zwischen zwei Kopfbällen desselben Spielers + # ── AFK-Erkennung ──────────────────────────────────────────────────────── + afk-warn-seconds: 20 # Sekunden Stillstand bis zur ersten AFK-Warnung + afk-kick-seconds: 40 # Sekunden Stillstand bis zur Disqualifikation + afk-move-threshold: 0.5 # Mindestbewegung in Blöcken pro Sekunde + # ── Nachspielzeit ──────────────────────────────────────────────────────── injury-time-enabled: true injury-time-per-goal: 30 # Sekunden Nachspielzeit pro Tor @@ -42,7 +47,21 @@ gameplay: injury-time-per-foul: 5 # Sekunden pro Foul injury-time-per-out: 3 # Sekunden pro Aus-Situation -# ── Nachrichten (alle editierbar) ────────────────── + # ── Anstoß-Kreis ───────────────────────────────────────────────────────── + kickoff-circle-radius: 9.15 # Pflichtabstand für Gegner beim Anstoß (Blöcke, FIFA: 9.15m) + + # ── Handball ───────────────────────────────────────────────────────────── + handball-enabled: true # Handspiel-Erkennung an/aus (Shift + Ball auf Armhöhe) + + # ── Pässe ──────────────────────────────────────────────────────────────── + long-pass-distance: 20.0 # Ab wie vielen Blöcken ein Pass als „Langpass" gilt + +# ── Stadionatmosphäre ──────────────────────────────────────────────────────── +atmosphere: + enabled: true + goal-fireworks: 5 # Anzahl Feuerwerke bei einem Tor (0 = deaktiviert) + +# ── Nachrichten (alle editierbar) ───────────────────────────────────────────── # Verfügbare Platzhalter je nach Kontext: # {player} = Spielername # {team} = Teamname @@ -115,9 +134,9 @@ messages: 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-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!" # Eigentore & Assists @@ -126,12 +145,15 @@ messages: assist: "§7Vorlage: §e{player}" # Nachspielzeit - injury-time: "§c⏱ §l+{n} Min. Nachspielzeit!" - injury-time-bar: "§c+{n}' §8│ " + injury-time: "§c⏱ §l+{n} Nachspielzeit!" + injury-time-bar: "§c+{n} §8│ " # Anstoß kickoff-team: "§e⚽ §7Anstoß für {team}§7!" + # Anstoß-Kreis + kickoff-circle: "§cAbstand halten! §7Anstoß-Kreis ({n}m)" + # Strafraum / Elfmeter bei Foul foul-penalty: "§c⚠ §lFOUL IM STRAFRAUM! §7Elfmeter für {team}§7!" @@ -142,6 +164,26 @@ messages: # Kopfball header: "§e⚽ §7Kopfball von §e{player}§7!" + # Handball + handball: "§e✋ §cHANDSPIEL §7von §e{player}§7!" + handball-title: "§c✋ HANDSPIEL!" + handball-penalty: "§c⚠ ELFMETER! §7Handspiel im Strafraum!" + + # Drop Ball / Schiedsrichterball + dropball: "§8[Schiri] §7⬇ §eSchiedsrichterball §7– beide Teams dürfen spielen!" + dropball-title: "§e⬇ DROPBALL" + dropball-sub: "§7Beide Teams – los!" + + # Pässe + long-pass: "§7⚽ §eLangpass §7von §f{player} §7zu §f{target} §8({n} Blöcke)" + + # Teamwahl + team-request: "§7Teamwunsch gesetzt: {team}§7. Wird beim nächsten Spielstart berücksichtigt." + team-request-fail: "§cDein Wunsch-Team war zu groß – du wurdest automatisch zugeteilt." + + # Trikot-Nummern + jersey-info: "§7Deine Trikot-Nummer: §e#{n}" + # Spieler beitreten / verlassen player-join: "§e{player} §7ist beigetreten! §8({n}/{max})" player-leave: "§e{player} §7hat das Spiel verlassen!" @@ -149,11 +191,12 @@ messages: 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 + report-header: "§e§l━━━━━━ MATCHBERICHT ━━━━━━" + report-footer: "§e§l━━━━━━━━━━━━━━━━━━━━━━━━━" + report-goals: "§7Tore:" + report-cards: "§7Karten:" + report-fouls: "§7Fouls:" + report-offside: "§7Abseits:" + report-possession: "§7Ballbesitz: §c{n}% §7(Rot) vs §9{m}% §7(Blau)" + 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 index fc02b88..17320e2 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -19,5 +19,5 @@ permissions: commands: fussball: description: Hauptbefehl des Fußball-Plugins - usage: /fussball + usage: /fussball aliases: [fb, soccer] \ No newline at end of file