Update from Git Manager GUI

This commit is contained in:
2026-02-28 08:38:01 +01:00
parent 6de8184d0a
commit ce1b67754d
11 changed files with 660 additions and 62 deletions

View File

@@ -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; }
}

View File

@@ -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 {

View File

@@ -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 <arena> 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<Map<?, ?>> 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 <arena>")); 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 <id> 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 <arena> §7- Spiel beitreten");
s.sendMessage("§e/fb leave §7- Spiel / Zuschauer verlassen");
s.sendMessage("§e/fb spectate <arena> §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<String> onTabComplete(CommandSender sender, Command cmd, String alias, String[] args) {
List<String> 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));

View File

@@ -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;
@@ -48,6 +49,23 @@ public class Game {
private final Map<UUID, Integer> goals = new HashMap<>();
private final Map<UUID, Integer> 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<UUID, Team> 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<UUID, Integer> 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<UUID> 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<String> goalLines = matchEvents.stream().filter(e -> e.contains("TOR:")).collect(Collectors.toList());
List<String> cardLines = matchEvents.stream().filter(e -> e.contains("🟨") || e.contains("🟥")).collect(Collectors.toList());
List<String> 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<String> 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<UUID> 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 (0100), -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 (0100), -1 wenn noch keine Daten */
public int getBluePossessionPercent() {
if (totalPossessionTicks == 0) return -1;
return (int) Math.round((double) bluePossessionTicks / totalPossessionTicks * 100.0);
}
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);

View File

@@ -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<Map<?, ?>> matches = new ArrayList<>(
config.contains("matches") ? config.getMapList("matches") : new ArrayList<>());
Map<String, Object> 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<Map<?, ?>> getMatches(int limit) {
if (!config.contains("matches")) return Collections.emptyList();
List<Map<?, ?>> all = config.getMapList("matches");
return all.subList(0, Math.min(limit, all.size()));
}
/** Gibt alle gespeicherten Spiele zurück. */
public List<Map<?, ?>> 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<String> goalEvents;
public MatchRecord(String arena,
int redScore, int blueScore,
String winner,
int durationSeconds,
int redPossession, int bluePossession,
int penaltyRed, int penaltyBlue,
List<String> 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();
}
}
}

View File

@@ -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;

View File

@@ -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."
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."

View File

@@ -19,5 +19,5 @@ permissions:
commands:
fussball:
description: Hauptbefehl des Fußball-Plugins
usage: /fussball <join|leave|spectate|list|stats|top|create|delete|setup|stop|debug>
usage: /fussball <join|leave|spectate|team|list|stats|top|history|create|delete|setup|stop|setgk|dropball|debug|hologram>
aliases: [fb, soccer]