11 Commits
1.0.1 ... main

Author SHA1 Message Date
a0085ba5bd Update from Git Manager GUI 2026-03-27 15:18:24 +01:00
e10f1313bd Upload via Git Manager GUI - pom.xml 2026-03-27 14:18:23 +00:00
960290dfa8 Update from Git Manager GUI 2026-03-27 10:05:43 +01:00
3f6d91cdc7 Upload via Git Manager GUI - pom.xml 2026-03-27 09:05:42 +00:00
54414cbc2e Upload file README.md via GUI 2026-03-17 16:15:13 +01:00
885fd9792d Upload file pom.xml via GUI 2026-03-17 16:15:07 +01:00
a6d17f4d64 Update from Git Manager GUI 2026-03-17 16:15:01 +01:00
22c5837455 Upload file pom.xml via GUI 2026-03-17 15:20:25 +01:00
266268bd0b Update from Git Manager GUI 2026-03-17 15:20:20 +01:00
c837c963af Upload file README.md via GUI 2026-03-17 15:20:18 +01:00
d7ed7b00c9 Upload file README.md via GUI 2026-03-17 13:07:37 +01:00
11 changed files with 941 additions and 111 deletions

114
README.md
View File

@@ -23,6 +23,7 @@ Ein professionelles Fußball-Plugin für Spigot/Paper 1.21+ mit echtem Ball-Phys
| 🙋 Teamwahl | Spieler können per `/fb team rot|blau` ihr Wunsch-Team wählen (vor Spielstart) | | 🙋 Teamwahl | Spieler können per `/fb team rot|blau` ihr Wunsch-Team wählen (vor Spielstart) |
| 👕 Team-Rüstung | Farbige Leder-Rüstung mit **Trikot-Nummer** (#1, #2, …) auf dem Brustpanzer | | 👕 Team-Rüstung | Farbige Leder-Rüstung mit **Trikot-Nummer** (#1, #2, …) auf dem Brustpanzer |
| 🥅 Tor-Erkennung | Region-basierte Tore mit Trajectory-Check (Schritt-für-Schritt Bahnverfolgung) | | 🥅 Tor-Erkennung | Region-basierte Tore mit Trajectory-Check (Schritt-für-Schritt Bahnverfolgung) |
| 💠 Tor-Beacons | Beacon-Farbe am linken/rechten Tor zeigt das verteidigende Team (Rot/Blau) und wechselt automatisch zur Halbzeit mit den Seiten |
| 🎉 Tor-Effekte | Feuerwerk, Titel, Action-Bar Tor-Replay, Sound | | 🎉 Tor-Effekte | Feuerwerk, Titel, Action-Bar Tor-Replay, Sound |
| 🎶 Stadionatmosphäre | Jubel-Sounds & mehrfache Feuerwerke beim Tor; Enttäuschungs-Sound für das andere Team | | 🎶 Stadionatmosphäre | Jubel-Sounds & mehrfache Feuerwerke beim Tor; Enttäuschungs-Sound für das andere Team |
| 📊 Scoreboard | Live-Spielstand, Zeit, Halbzeit-Anzeige, Ballbesitz, farbige Spielernamen über Kopf | | 📊 Scoreboard | Live-Spielstand, Zeit, Halbzeit-Anzeige, Ballbesitz, farbige Spielernamen über Kopf |
@@ -48,7 +49,8 @@ Ein professionelles Fußball-Plugin für Spigot/Paper 1.21+ mit echtem Ball-Phys
| 🏟 Strafraum | Auto-berechnet aus Tor-Koordinaten oder manuell setzbar | | 🏟 Strafraum | Auto-berechnet aus Tor-Koordinaten oder manuell setzbar |
| 📋 Matchbericht | Tore, Karten, Fouls, Abseits, Ballbesitz und MVP am Spielende | | 📋 Matchbericht | Tore, Karten, Fouls, Abseits, Ballbesitz und MVP am Spielende |
| 🏆 MVP-System | Bester Torschütze wird nach dem Spiel bekannt gegeben | | 🏆 MVP-System | Bester Torschütze wird nach dem Spiel bekannt gegeben |
| 📈 Persistente Stats | Tore, Eigentore, Vorlagen, Schüsse, Siege, Niederlagen, Siegquote | | 📈 Persistente Stats | Tore, Eigentore, Vorlagen, Schüsse, Siege, Niederlagen, Siegquote; per Admin-Befehl resetbar |
| 🪄 Hologramme | Platzierbare Ingame-Hologramme für Top-Tore, Top-Siege und Live-Matchstand; Texte ingame bearbeitbar, Standard-Farben über `config.yml` anpassbar |
| 📋 Match-History | Letzte 50 Spiele dauerhaft gespeichert, abrufbar per `/fb history` | | 📋 Match-History | Letzte 50 Spiele dauerhaft gespeichert, abrufbar per `/fb history` |
| 🔢 Warteschlange | Automatische Queue wenn Arena voll; nächster Spieler rückt nach | | 🔢 Warteschlange | Automatische Queue wenn Arena voll; nächster Spieler rückt nach |
| 👀 Zuschauer-Modus | Sichtbarer Zuschauer-Bereich außerhalb des Feldes mit BossBar und Scoreboard | | 👀 Zuschauer-Modus | Sichtbarer Zuschauer-Bereich außerhalb des Feldes mit BossBar und Scoreboard |
@@ -164,11 +166,93 @@ Alle Pflichtfelder müssen **§a✔** zeigen. Erst dann ist die Arena spielberei
| `/fb setgk <arena> <spieler>` | Torwart eines laufenden Spiels manuell neu zuweisen | | `/fb setgk <arena> <spieler>` | Torwart eines laufenden Spiels manuell neu zuweisen |
| `/fb dropball <arena>` | Schiedsrichterball Ball neutral spawnen, beide Teams dürfen spielen | | `/fb dropball <arena>` | Schiedsrichterball Ball neutral spawnen, beide Teams dürfen spielen |
| `/fb debug <arena>` | Tor-/Feld-Regionen, Ball-Position und Aus-Seite debuggen | | `/fb debug <arena>` | Tor-/Feld-Regionen, Ball-Position und Aus-Seite debuggen |
| `/fb stats reset <spieler\|all>` | Einzelne Spieler-Stats oder alle Statistiken zurücksetzen |
| `/fb hologram set <arena> goals\|wins\|match` | Hologramm für eine Arena an deiner aktuellen Position erstellen |
| `/fb hologram remove` | Nächstes Hologramm im 5-Block-Radius entfernen |
| `/fb hologram delete <arena> goals\|wins\|match` | Hologramm einer Arena gezielt löschen |
| `/fb hologram text <arena> goals\|wins\|match <zeile> <text>` | Einzelne Hologramm-Zeilen bearbeiten |
| `/fb hologram textpreview <arena> goals\|wins\|match` | Aktuell gespeicherte Hologramm-Zeilen anzeigen |
| `/fb hologram textreset <arena> goals\|wins\|match` | Standard-Text eines Hologramms wiederherstellen |
| `/fb hologram list` | Alle Hologramme als `arena → typ` anzeigen |
| `/fb hologram reload` | Hologramme sowie Hologramm-Farben aus `holograms.yml`/`config.yml` neu laden |
Alle Commands unterstützen **Tab-Completion** für Arena-Namen und Optionen. Alle Commands unterstützen **Tab-Completion** für Arena-Namen und Optionen.
--- ---
## 🪄 Hologramme
Hologramme kannst du **direkt ingame** setzen: Stelle dich an die gewünschte Position und nutze den Befehl.
```
/fb hologram set <arena> goals → Top-10 Torschützen
/fb hologram set <arena> wins → Top-10 Gewinner
/fb hologram set <arena> match → Großes Live-Hologramm (nur Spielstand + Zeit/Nachspielzeit)
```
Beispiel für deine Arena:
```
/fb hologram set allianz goals
/fb hologram set allianz wins
/fb hologram set allianz match
```
Intern speichert das Plugin diese Hologramme getrennt pro Arena und Typ, also z. B. `allianz_goals`, `allianz_wins` und `allianz_match`.
Weitere Verwaltung:
```
/fb hologram remove
/fb hologram delete <arena> goals|wins|match
/fb hologram text <arena> goals|wins|match <zeile> <text>
/fb hologram textpreview <arena> goals|wins|match
/fb hologram textreset <arena> goals|wins|match
/fb hologram list
/fb hologram reload
```
**Hinweise:**
- `<arena>` ist der Arena-Name, nicht die Hologramm-ID.
- `match` zeigt bewusst nur Spielstand + Zeit und aktualisiert sich automatisch bei Toren, Zeitlauf und Nachspielzeit.
- `goals`/`wins` lassen sich per Rechtsklick direkt am Hologramm umschalten.
- Farben und Formatierungen für einzelne Zeilen kannst du über `&`-Codes im `text`-Befehl setzen, z. B. `&6&lTitel`, `&cRot`, `&oKursiv`.
- Standard-Farbschema (Titel, Werte, Trennlinie, Match-Header usw.) stellst du zentral in `config.yml` unter `holograms:` ein und übernimmst es mit `/fb hologram reload`.
- `text` bearbeitet immer genau eine Zeile. Fehlende Zeilen werden bei Bedarf automatisch ergänzt.
- Mit `textreset` wird nur das gewählte Hologramm auf den Standardtext zurückgesetzt.
### Hologramm-Platzhalter
Für `goals` und `wins` stehen in bearbeitbaren Texten diese Platzhalter zur Verfügung:
```
{title} → Standard-Überschrift des Hologramms
{separator} → Trennlinie
{entries} → Dynamische Top-10-Liste
{toggle} → Hinweis zum Umschalten per Rechtsklick
```
Für `match` stehen diese Platzhalter zur Verfügung:
```
{header} → Live-Match Überschrift
{separator} → Trennlinie
{phase} → Aktuelle Spielphase
{score} → Aktueller Spielstand
{time} → Spielzeit oder Nachspielzeit
```
Beispiel:
```
/fb hologram text allianz goals 1 &6&l⚽ Allianz Topscorer
/fb hologram text allianz goals 2 &8{separator}
/fb hologram text allianz goals 3 {entries}
/fb hologram text allianz goals 4 &7{toggle}
```
---
## 🎮 Spielablauf ## 🎮 Spielablauf
``` ```
@@ -392,6 +476,8 @@ Das Schild wird automatisch formatiert und mit Live-Status aktualisiert:
``` ```
/fb stats → Eigene Statistiken /fb stats → Eigene Statistiken
/fb stats Notch → Statistiken von "Notch" /fb stats Notch → Statistiken von "Notch"
/fb stats reset Notch → Statistiken von "Notch" zurücksetzen (Admin)
/fb stats reset all → Alle Statistiken zurücksetzen (Admin)
/fb top goals → Top 10 Torschützen /fb top goals → Top 10 Torschützen
/fb top wins → Top 10 nach Siegen (inkl. Siegquote) /fb top wins → Top 10 nach Siegen (inkl. Siegquote)
/fb top kicks → Top 10 nach Anzahl Schüssen /fb top kicks → Top 10 nach Anzahl Schüssen
@@ -412,7 +498,7 @@ Das Schild wird automatisch formatiert und mit Live-Status aktualisiert:
| 📊 Gespielte Spiele | Gesamtanzahl Spiele | | 📊 Gespielte Spiele | Gesamtanzahl Spiele |
| 📈 Siegquote | Siege / Spiele × 100 % | | 📈 Siegquote | Siege / Spiele × 100 % |
Alle Daten werden in `plugins/Fussball/stats.yml` dauerhaft gespeichert. Alle Daten werden in `plugins/Fussball/stats.yml` dauerhaft gespeichert und können über `/fb stats reset <spieler|all>` gezielt zurückgesetzt werden.
--- ---
@@ -443,7 +529,7 @@ Wenn PlaceholderAPI installiert ist, sind folgende Platzhalter verfügbar:
| Permission | Beschreibung | Standard | | Permission | Beschreibung | Standard |
|---|---|---| |---|---|---|
| `fussball.admin` | Alle Admin-Commands (create, delete, setup, stop, setgk, dropball, debug, Schilder, Global-Chat) | OP | | `fussball.admin` | Alle Admin-Commands inklusive Stats-Reset, Hologramm-Verwaltung, Schilder und Global-Chat | OP |
| `fussball.play` | Spielen, Zuschauen, Stats anzeigen | Alle | | `fussball.play` | Spielen, Zuschauen, Stats anzeigen | Alle |
--- ---
@@ -494,6 +580,21 @@ gameplay:
atmosphere: atmosphere:
enabled: true enabled: true
goal-fireworks: 5 # Feuerwerke pro Tor (0 = deaktiviert) goal-fireworks: 5 # Feuerwerke pro Tor (0 = deaktiviert)
holograms:
goals-title-color: "&6&l" # Titel-Farbe Top-Tore
goals-value-color: "&4" # Werte-Farbe Tore
wins-title-color: "&2&l" # Titel-Farbe Top-Siege
wins-value-color: "&2" # Werte-Farbe Siege
name-color: "&0" # Spielername-Farbe
label-color: "&8" # Labels (Tore/Siege/Trenner)
separator-color: "&8&m" # Trennlinien-Stil
toggle-color: "&8&o" # Umschalt-Hinweis
match-header-color: "&e&l" # Match-Header
match-score-red: "&c&l" # Score Rot
match-score-blue: "&9&l" # Score Blau
match-time-color: "&e" # Normale Spielzeit
match-injury-color: "&c" # Nachspielzeit
``` ```
Alle `messages`-Einträge sind ebenfalls vollständig anpassbar. Platzhalter: `{player}`, `{team}`, `{score}`, `{time}`, `{reason}`, `{n}`, `{max}`. Alle `messages`-Einträge sind ebenfalls vollständig anpassbar. Platzhalter: `{player}`, `{team}`, `{score}`, `{time}`, `{reason}`, `{n}`, `{max}`.
@@ -509,7 +610,7 @@ plugins/Fussball/
├── signs.yml → Gespeicherte Join-/Zuschauer-Schilder (auto-generiert) ├── signs.yml → Gespeicherte Join-/Zuschauer-Schilder (auto-generiert)
├── stats.yml → Spieler-Statistiken (auto-generiert) ├── stats.yml → Spieler-Statistiken (auto-generiert)
├── matchhistory.yml → Letzte 50 Spiele (auto-generiert) ├── matchhistory.yml → Letzte 50 Spiele (auto-generiert)
└── holograms.yml → Statistik-Hologramme (auto-generiert) └── holograms.yml → Statistik-Hologramme inkl. benutzerdefinierter Texte (auto-generiert)
``` ```
--- ---
@@ -519,6 +620,7 @@ plugins/Fussball/
- **Feldgröße:** Mindestens **30×20 Blöcke** empfohlen (größer = mehr Spaß) - **Feldgröße:** Mindestens **30×20 Blöcke** empfohlen (größer = mehr Spaß)
- **Tore:** Ca. **5 Blöcke breit, 3 Blöcke hoch** mindestens **2 Blöcke breit** da der Ball sonst stecken bleiben kann - **Tore:** Ca. **5 Blöcke breit, 3 Blöcke hoch** mindestens **2 Blöcke breit** da der Ball sonst stecken bleiben kann
- **Tortiefe:** Mindestens **2 Blöcke tief** damit Tor-Erkennung sauber funktioniert - **Tortiefe:** Mindestens **2 Blöcke tief** damit Tor-Erkennung sauber funktioniert
- **Tor-Beacons (optional):** Platziere je einen Beacon nahe jedem Tor. Das Plugin setzt automatisch rotes/blaues Glas darüber und tauscht die Farben nach der Halbzeit beim Seitenwechsel.
- **`fieldmin/fieldmax` immer setzen!** Sonst gibt es weder Aus-Erkennung noch Spieler-Feldgrenzenkontrolle - **`fieldmin/fieldmax` immer setzen!** Sonst gibt es weder Aus-Erkennung noch Spieler-Feldgrenzenkontrolle
- **`center` genau in die Feldmitte setzen** wird für den Anstoß-Kreis verwendet - **`center` genau in die Feldmitte setzen** wird für den Anstoß-Kreis verwendet
- **`ballspawn` genau in die Mitte** er ist gleichzeitig der Anstoßpunkt und der Spawn nach einem Tor - **`ballspawn` genau in die Mitte** er ist gleichzeitig der Anstoßpunkt und der Spawn nach einem Tor
@@ -549,6 +651,4 @@ Nach jedem Spiel wird automatisch ein Matchbericht ausgegeben:
--- ---
**Copyright © 2026 - M_Viper - Alle Rechte vorbehalten** *Erstellt mit ❤️ von M_Viper — viel Spaß beim Fußballspielen! 🏆*
Die unbefugte Vervielfältigung, Verbreitung oder Weitergabe dieses Plugins ist strafbar und wird rechtlich verfolgt.

View File

@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>de.fussball</groupId> <groupId>de.fussball</groupId>
<artifactId>Fussball</artifactId> <artifactId>Fussball</artifactId>
<version>1.0.0</version> <version>1.0.3</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Fussball</name> <name>Fussball</name>
<description>Ein vollständiges Fußball-Minigame für Minecraft</description> <description>Ein vollständiges Fußball-Minigame für Minecraft</description>

View File

@@ -11,6 +11,7 @@ import de.fussball.plugin.hologram.HologramManager;
import de.fussball.plugin.stats.MatchHistory; import de.fussball.plugin.stats.MatchHistory;
import de.fussball.plugin.stats.StatsManager; import de.fussball.plugin.stats.StatsManager;
import de.fussball.plugin.utils.MessageUtil; import de.fussball.plugin.utils.MessageUtil;
import org.bukkit.ChatColor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.command.*; import org.bukkit.command.*;
@@ -81,6 +82,36 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
} }
case "stats" -> { case "stats" -> {
if (args.length >= 2 && args[1].equalsIgnoreCase("reset")) {
if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; }
if (args.length < 3) { sender.sendMessage(MessageUtil.error("Benutze: /fb stats reset <spieler|all>")); return true; }
if (args[2].equalsIgnoreCase("all") || args[2].equalsIgnoreCase("*")) {
int removed = plugin.getStatsManager().resetAllStats();
plugin.getHologramManager().refreshAll();
sender.sendMessage(MessageUtil.success("Alle Statistiken zurückgesetzt! §7(" + removed + " Einträge)"));
return true;
}
Player onlineTarget = Bukkit.getPlayerExact(args[2]);
UUID targetUuid = onlineTarget != null
? onlineTarget.getUniqueId()
: plugin.getStatsManager().findPlayerUuidByName(args[2]);
if (targetUuid == null) {
sender.sendMessage(MessageUtil.error("Keine gespeicherten Statistiken für §e" + args[2] + " §cgefunden!"));
return true;
}
if (!plugin.getStatsManager().resetStats(targetUuid)) {
sender.sendMessage(MessageUtil.error("Statistiken von §e" + args[2] + " §ckonnten nicht zurückgesetzt werden."));
return true;
}
plugin.getHologramManager().refreshAll();
sender.sendMessage(MessageUtil.success("Statistiken von §e" + args[2] + " §azurückgesetzt!"));
return true;
}
if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; } if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; }
Player target = args.length >= 2 ? Bukkit.getPlayer(args[1]) : player; Player target = args.length >= 2 ? Bukkit.getPlayer(args[1]) : player;
if (target == null) { player.sendMessage(MessageUtil.error("Spieler §e" + args[1] + " §cnicht gefunden!")); return true; } if (target == null) { player.sendMessage(MessageUtil.error("Spieler §e" + args[1] + " §cnicht gefunden!")); return true; }
@@ -281,9 +312,9 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
} }
// ── Hologramm-Verwaltung ───────────────────────────────────────── // ── Hologramm-Verwaltung ─────────────────────────────────────────
// /fb hologram set <id> goals|wins|match Hologramm erstellen // /fb hologram set <arena> goals|wins|match Hologramm erstellen/verschieben
// /fb hologram remove Nächstes Hologramm (< 5 Blöcke) entfernen // /fb hologram remove Nächstes Hologramm (< 5 Blöcke) entfernen
// /fb hologram delete <id> Hologramm nach ID löschen // /fb hologram delete <arena> goals|wins|match Hologramm gezielt löschen
// /fb hologram reload Alle Hologramme neu spawnen // /fb hologram reload Alle Hologramme neu spawnen
// /fb hologram list Alle Hologramme anzeigen // /fb hologram list Alle Hologramme anzeigen
case "hologram", "holo" -> { case "hologram", "holo" -> {
@@ -291,9 +322,12 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; } if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; }
if (args.length < 2) { if (args.length < 2) {
player.sendMessage(MessageUtil.header("Hologramm-Befehle")); player.sendMessage(MessageUtil.header("Hologramm-Befehle"));
player.sendMessage("§e/fb hologram set <id> goals|wins|match §7 Hologramm setzen"); player.sendMessage("§e/fb hologram set <arena> goals|wins|match §7 Hologramm setzen");
player.sendMessage("§e/fb hologram remove §7 Nächstes entfernen (< 5 Blöcke)"); player.sendMessage("§e/fb hologram remove §7 Nächstes entfernen (< 5 Blöcke)");
player.sendMessage("§e/fb hologram delete <id> §7 Nach ID löschen"); player.sendMessage("§e/fb hologram delete <arena> goals|wins|match §7 Gezielt löschen");
player.sendMessage("§e/fb hologram text <arena> goals|wins|match <zeile> <text> §7 Textzeile anpassen");
player.sendMessage("§e/fb hologram textpreview <arena> goals|wins|match §7 Aktuelles Textlayout ansehen");
player.sendMessage("§e/fb hologram textreset <arena> goals|wins|match §7 Standardtext wiederherstellen");
player.sendMessage("§e/fb hologram reload §7 Alle neu laden"); player.sendMessage("§e/fb hologram reload §7 Alle neu laden");
player.sendMessage("§e/fb hologram list §7 Alle anzeigen"); player.sendMessage("§e/fb hologram list §7 Alle anzeigen");
player.sendMessage("§7Gesamt: §e" + plugin.getHologramManager().getCount() + " §7Hologramme"); player.sendMessage("§7Gesamt: §e" + plugin.getHologramManager().getCount() + " §7Hologramme");
@@ -303,22 +337,26 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
switch (args[1].toLowerCase()) { switch (args[1].toLowerCase()) {
case "set" -> { case "set" -> {
if (args.length < 4) { if (args.length < 4) {
player.sendMessage(MessageUtil.error("Benutze: /fb hologram set <id> goals|wins|match")); player.sendMessage(MessageUtil.error("Benutze: /fb hologram set <arena> goals|wins|match"));
return true;
}
Arena arena = plugin.getArenaManager().getArena(args[2]);
if (arena == null) {
player.sendMessage(MessageUtil.error("Arena §e" + args[2] + " §cnicht gefunden!"));
return true; return true;
} }
String id = args[2];
FussballHologram.HoloType type = switch (args[3].toLowerCase()) { FussballHologram.HoloType type = switch (args[3].toLowerCase()) {
case "wins", "siege" -> FussballHologram.HoloType.WINS; case "wins", "siege" -> FussballHologram.HoloType.WINS;
case "match", "live", "game" -> FussballHologram.HoloType.MATCH; case "match", "live", "game" -> FussballHologram.HoloType.MATCH;
default -> FussballHologram.HoloType.GOALS; default -> FussballHologram.HoloType.GOALS;
}; };
plugin.getHologramManager().createHologram(id, player.getLocation(), type); String id = buildHologramId(arena.getName(), type);
String holoLabel = switch (type) { plugin.getHologramManager().removeHologram(id);
case WINS -> "Top-10-Siege"; if (!plugin.getHologramManager().createHologram(id, player.getLocation(), type)) {
case MATCH -> "Live-Match"; player.sendMessage(MessageUtil.error("Konnte das Hologramm für Arena §e" + arena.getName() + " §cund Typ §e" + hologramTypeName(type) + " §cnicht setzen."));
default -> "Top-10-Tore"; return true;
}; }
player.sendMessage(MessageUtil.success("§e" + id + " §a(" + holoLabel + ") Hologramm gesetzt!")); player.sendMessage(MessageUtil.success("Hologramm §e" + hologramTypeName(type) + " §afür Arena §e" + arena.getName() + " §agesetzt!"));
if (type == FussballHologram.HoloType.MATCH) { if (type == FussballHologram.HoloType.MATCH) {
player.sendMessage("§7§oLive-Match-Hologramm aktualisiert sich automatisch bei Toren und Nachspielzeit."); player.sendMessage("§7§oLive-Match-Hologramm aktualisiert sich automatisch bei Toren und Nachspielzeit.");
} else { } else {
@@ -334,13 +372,124 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
} }
} }
case "delete" -> { case "delete" -> {
if (args.length < 3) { player.sendMessage(MessageUtil.error("Benutze: /fb hologram delete <id>")); return true; } if (args.length >= 4) {
Arena arena = plugin.getArenaManager().getArena(args[2]);
if (arena == null) {
player.sendMessage(MessageUtil.error("Arena §e" + args[2] + " §cnicht gefunden!"));
return true;
}
FussballHologram.HoloType type = switch (args[3].toLowerCase()) {
case "wins", "siege" -> FussballHologram.HoloType.WINS;
case "match", "live", "game" -> FussballHologram.HoloType.MATCH;
default -> FussballHologram.HoloType.GOALS;
};
String id = buildHologramId(arena.getName(), type);
if (plugin.getHologramManager().removeHologram(id)) {
player.sendMessage(MessageUtil.success("Hologramm §e" + hologramTypeName(type) + " §ader Arena §e" + arena.getName() + " §agelöscht!"));
} else {
player.sendMessage(MessageUtil.error("Kein Hologramm vom Typ §e" + hologramTypeName(type) + " §cfür Arena §e" + arena.getName() + " §cgefunden!"));
}
return true;
}
if (args.length < 3) { player.sendMessage(MessageUtil.error("Benutze: /fb hologram delete <arena> goals|wins|match")); return true; }
if (plugin.getHologramManager().removeHologram(args[2])) { if (plugin.getHologramManager().removeHologram(args[2])) {
player.sendMessage(MessageUtil.success("Hologramm §e" + args[2] + " §agelöscht!")); player.sendMessage(MessageUtil.success("Legacy-Hologramm §e" + args[2] + " §agelöscht!"));
} else { } else {
player.sendMessage(MessageUtil.error("Kein Hologramm mit ID §e" + args[2] + "§c gefunden!")); player.sendMessage(MessageUtil.error("Kein Hologramm mit ID §e" + args[2] + "§c gefunden!"));
} }
} }
case "text" -> {
if (args.length < 6) {
player.sendMessage(MessageUtil.error("Benutze: /fb hologram text <arena> goals|wins|match <zeile> <text>"));
return true;
}
Arena arena = plugin.getArenaManager().getArena(args[2]);
if (arena == null) {
player.sendMessage(MessageUtil.error("Arena §e" + args[2] + " §cnicht gefunden!"));
return true;
}
FussballHologram.HoloType type = parseHologramType(args[3]);
int line;
try {
line = Integer.parseInt(args[4]);
} catch (NumberFormatException ex) {
player.sendMessage(MessageUtil.error("§e" + args[4] + " §cist keine gültige Zeilennummer!"));
return true;
}
if (line < 1) {
player.sendMessage(MessageUtil.error("Zeilennummer muss mindestens §e1 §csein!"));
return true;
}
String id = buildHologramId(arena.getName(), type);
HologramManager hologramManager = plugin.getHologramManager();
if (hologramManager.getHologram(id) == null) {
player.sendMessage(MessageUtil.error("Hologramm §e" + hologramTypeName(type) + " §cfür Arena §e" + arena.getName() + " §cexistiert nicht!"));
return true;
}
List<String> lines = new ArrayList<>(getEditableHologramTemplate(hologramManager, id, type));
while (lines.size() < line) {
lines.add(" ");
}
lines.set(line - 1, ChatColor.translateAlternateColorCodes('&', joinArgs(args, 5)));
if (!hologramManager.setCustomText(id, type, lines)) {
player.sendMessage(MessageUtil.error("Konnte den Hologramm-Text nicht speichern!"));
return true;
}
player.sendMessage(MessageUtil.success("Hologramm-Text für §e" + arena.getName() + " §a(" + hologramTypeName(type) + ") aktualisiert."));
player.sendMessage("§7Zeile §e" + line + "§7: " + lines.get(line - 1));
}
case "textpreview" -> {
if (args.length < 4) {
player.sendMessage(MessageUtil.error("Benutze: /fb hologram textpreview <arena> goals|wins|match"));
return true;
}
Arena arena = plugin.getArenaManager().getArena(args[2]);
if (arena == null) {
player.sendMessage(MessageUtil.error("Arena §e" + args[2] + " §cnicht gefunden!"));
return true;
}
FussballHologram.HoloType type = parseHologramType(args[3]);
String id = buildHologramId(arena.getName(), type);
HologramManager hologramManager = plugin.getHologramManager();
if (hologramManager.getHologram(id) == null) {
player.sendMessage(MessageUtil.error("Hologramm §e" + hologramTypeName(type) + " §cfür Arena §e" + arena.getName() + " §cexistiert nicht!"));
return true;
}
List<String> lines = getEditableHologramTemplate(hologramManager, id, type);
player.sendMessage(MessageUtil.header("Holo-Text: " + arena.getName() + " / " + hologramTypeName(type)));
for (int i = 0; i < lines.size(); i++) {
player.sendMessage("§e" + (i + 1) + "§7: " + lines.get(i));
}
if (type == FussballHologram.HoloType.MATCH) {
player.sendMessage("§8Platzhalter: §7{header} {separator} {phase} {score} {time}");
} else {
player.sendMessage("§8Platzhalter: §7{title} {separator} {entries} {toggle}");
}
}
case "textreset" -> {
if (args.length < 4) {
player.sendMessage(MessageUtil.error("Benutze: /fb hologram textreset <arena> goals|wins|match"));
return true;
}
Arena arena = plugin.getArenaManager().getArena(args[2]);
if (arena == null) {
player.sendMessage(MessageUtil.error("Arena §e" + args[2] + " §cnicht gefunden!"));
return true;
}
FussballHologram.HoloType type = parseHologramType(args[3]);
String id = buildHologramId(arena.getName(), type);
HologramManager hologramManager = plugin.getHologramManager();
if (hologramManager.getHologram(id) == null) {
player.sendMessage(MessageUtil.error("Hologramm §e" + hologramTypeName(type) + " §cfür Arena §e" + arena.getName() + " §cexistiert nicht!"));
return true;
}
hologramManager.resetCustomText(id, type);
player.sendMessage(MessageUtil.success("Standardtext für §e" + arena.getName() + " §a(" + hologramTypeName(type) + ") wiederhergestellt."));
}
case "reload" -> { case "reload" -> {
plugin.getHologramManager().reload(); plugin.getHologramManager().reload();
player.sendMessage(MessageUtil.success("Hologramme neu geladen! §7(" + plugin.getHologramManager().getCount() + " gesamt)")); player.sendMessage(MessageUtil.success("Hologramme neu geladen! §7(" + plugin.getHologramManager().getCount() + " gesamt)"));
@@ -351,11 +500,11 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
player.sendMessage(MessageUtil.warn("Keine Hologramme vorhanden.")); player.sendMessage(MessageUtil.warn("Keine Hologramme vorhanden."));
} else { } else {
for (String id : plugin.getHologramManager().getHologramIds()) { for (String id : plugin.getHologramManager().getHologramIds()) {
player.sendMessage("§7 • §e" + id); player.sendMessage("§7 • §e" + formatHologramDisplay(id));
} }
} }
} }
default -> player.sendMessage(MessageUtil.error("Gültig: set <id> goals|wins|match | remove | delete <id> | reload | list")); default -> player.sendMessage(MessageUtil.error("Gültig: set | remove | delete | text | textpreview | textreset | reload | list"));
} }
} }
@@ -494,7 +643,8 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
s.sendMessage("§e/fb history [n] §7- Letzte Spiele anzeigen"); s.sendMessage("§e/fb history [n] §7- Letzte Spiele anzeigen");
if (s.hasPermission("fussball.admin")) { if (s.hasPermission("fussball.admin")) {
s.sendMessage("§c§lAdmin: §ccreate / delete / setup / stop / debug / dropball"); s.sendMessage("§c§lAdmin: §ccreate / delete / setup / stop / debug / dropball");
s.sendMessage("§c§lAdmin: §chologram set goals|wins|match / remove / reload"); s.sendMessage("§c§lAdmin: §cstats reset <spieler|all>");
s.sendMessage("§c§lAdmin: §chologram set|delete|text|textpreview|textreset <arena> <typ>");
} }
} }
@@ -512,6 +662,43 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
private String yn(boolean b) { return b ? "§aJA" : "§cNEIN"; } private String yn(boolean b) { return b ? "§aJA" : "§cNEIN"; }
private String locStr(Location l) { return fmt(l.getX()) + " / " + fmt(l.getY()) + " / " + fmt(l.getZ()); } private String locStr(Location l) { return fmt(l.getX()) + " / " + fmt(l.getY()) + " / " + fmt(l.getZ()); }
private String fmt(double d) { return String.format("%.1f", d); } private String fmt(double d) { return String.format("%.1f", d); }
private String buildHologramId(String arenaName, FussballHologram.HoloType type) { return arenaName.toLowerCase() + "_" + hologramTypeName(type); }
private String joinArgs(String[] args, int start) { return String.join(" ", Arrays.copyOfRange(args, start, args.length)); }
private List<String> getEditableHologramTemplate(HologramManager hologramManager, String id, FussballHologram.HoloType type) {
List<String> custom = hologramManager.getCustomText(id, type);
return custom.isEmpty() ? getDefaultHologramTemplate(type) : custom;
}
private List<String> getDefaultHologramTemplate(FussballHologram.HoloType type) {
if (type == FussballHologram.HoloType.MATCH) {
return List.of("{header}", "{separator}", "{phase}", "{score}", "{time}");
}
return List.of("{title}", "{separator}", "{entries}", "{separator}", "{toggle}");
}
private FussballHologram.HoloType parseHologramType(String rawType) {
return switch (rawType.toLowerCase()) {
case "wins", "siege" -> FussballHologram.HoloType.WINS;
case "match", "live", "game" -> FussballHologram.HoloType.MATCH;
default -> FussballHologram.HoloType.GOALS;
};
}
private String hologramTypeName(FussballHologram.HoloType type) {
return switch (type) {
case WINS -> "wins";
case MATCH -> "match";
default -> "goals";
};
}
private String formatHologramDisplay(String id) {
int split = id.lastIndexOf('_');
if (split > 0 && split < id.length() - 1) {
String arena = id.substring(0, split);
String type = id.substring(split + 1);
if (type.equals("goals") || type.equals("wins") || type.equals("match")) {
return arena + " §7→ §e" + type;
}
}
return id;
}
// ── Tab-Completion ─────────────────────────────────────────────────────── // ── Tab-Completion ───────────────────────────────────────────────────────
@@ -523,14 +710,21 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
if (sender.hasPermission("fussball.admin")) list.addAll(List.of("create", "delete", "setup", "stop", "setgk", "debug", "hologram", "dropball")); 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())) { } else if (args.length == 2 && List.of("join","delete","setup","stop","setgk","debug","spectate","dropball").contains(args[0].toLowerCase())) {
list.addAll(plugin.getArenaManager().getArenaNames()); list.addAll(plugin.getArenaManager().getArenaNames());
} else if (args.length == 2 && args[0].equalsIgnoreCase("stats") && sender.hasPermission("fussball.admin")) {
list.addAll(List.of("reset"));
} else if (args.length == 3 && args[0].equalsIgnoreCase("stats") && args[1].equalsIgnoreCase("reset") && sender.hasPermission("fussball.admin")) {
list.add("all");
for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
list.add(onlinePlayer.getName());
}
} else if (args.length == 2 && args[0].equalsIgnoreCase("hologram")) { } else if (args.length == 2 && args[0].equalsIgnoreCase("hologram")) {
list.addAll(List.of("set", "remove", "delete", "reload", "list")); list.addAll(List.of("set", "remove", "delete", "text", "textpreview", "textreset", "reload", "list"));
} else if (args.length == 3 && args[0].equalsIgnoreCase("hologram") && args[1].equalsIgnoreCase("set")) { } else if (args.length == 3 && args[0].equalsIgnoreCase("hologram") && List.of("set", "delete", "text", "textpreview", "textreset").contains(args[1].toLowerCase())) {
list.addAll(plugin.getArenaManager().getArenaNames()); // id-Vorschläge (frei wählbar, aber arena-namen passen) list.addAll(plugin.getArenaManager().getArenaNames());
} else if (args.length == 4 && args[0].equalsIgnoreCase("hologram") && args[1].equalsIgnoreCase("set")) { } else if (args.length == 4 && args[0].equalsIgnoreCase("hologram") && List.of("set", "delete", "text", "textpreview", "textreset").contains(args[1].toLowerCase())) {
list.addAll(List.of("goals", "wins", "match")); list.addAll(List.of("goals", "wins", "match"));
} else if (args.length == 3 && args[0].equalsIgnoreCase("hologram") && args[1].equalsIgnoreCase("delete")) { } else if (args.length == 5 && args[0].equalsIgnoreCase("hologram") && args[1].equalsIgnoreCase("text")) {
list.addAll(plugin.getHologramManager().getHologramIds()); list.addAll(List.of("1", "2", "3", "4", "5"));
} else if (args.length == 3 && args[0].equalsIgnoreCase("setgk")) { } else if (args.length == 3 && args[0].equalsIgnoreCase("setgk")) {
// Spielernamen aus dem aktiven Spiel vorschlagen // Spielernamen aus dem aktiven Spiel vorschlagen
Game gkGame = plugin.getGameManager().getGame(args[1]); Game gkGame = plugin.getGameManager().getGame(args[1]);

View File

@@ -78,7 +78,10 @@ public class Game {
private UUID lastKicker = null; private UUID lastKicker = null;
private UUID secondLastKicker = null; // für Assist-Erkennung private UUID secondLastKicker = null; // für Assist-Erkennung
private boolean lastKickWasHeader = false; // für Rückpass-Regel (Header erlaubt) private boolean lastKickWasHeader = false; // für Rückpass-Regel (Header erlaubt)
/** true wenn der letzte Schuss ein Restart-Kick war (Einwurf/Eckstoß/Abstoß/Anstoß)
* → der NÄCHSTE Empfänger darf lt. Regel 11 §3 nicht auf Abseits geprüft werden */
private boolean lastKickWasRestart = false;
private Team lastTouchTeam = null; private Team lastTouchTeam = null;
private Team throwInTeam = null; private Team throwInTeam = null;
@@ -389,6 +392,10 @@ public class Game {
cd--; cd--;
} else { } else {
spawnBallDelayed(arena.getBallSpawn()); spawnBallDelayed(arena.getBallSpawn());
// Regel 8: Rot führt den Anstoß in der 1. Halbzeit aus
throwInTeam = Team.RED;
kickoffEnforceTicks = 200; // 10s Kreisschutz (Regel 8: 9,15m Abstand)
broadcastAll(Messages.get("kickoff-team", "team", "§cRotes Team"));
for (UUID uuid : allPlayers) { for (UUID uuid : allPlayers) {
Player p = Bukkit.getPlayer(uuid); Player p = Bukkit.getPlayer(uuid);
if (p != null) { if (p != null) {
@@ -444,9 +451,19 @@ public class Game {
secondHalf = true; secondHalf = true;
timeLeft = arena.getGameDuration() / 2; timeLeft = arena.getGameDuration() / 2;
updateGoalBeaconColors(); updateGoalBeaconColors();
// Nachspielzeit der 1. Halbzeit zurücksetzen // Nachspielzeit und Spielzustand der 1. Halbzeit zurücksetzen
injuryTimeBuffer = 0; injuryTimeBuffer = 0;
inInjuryTime = false; inInjuryTime = false;
lastKicker = null;
secondLastKicker = null;
lastTouchTeam = null;
lastKickWasHeader = false;
lastKickWasRestart = false;
lastBallLocation = null;
outCooldown = false;
offsideCooldown = false;
headerCooldowns.clear();
outOfBoundsCountdown.clear();
// Seitenwechsel: Rotes Team → BlueSpawn, Blaues Team → RedSpawn // Seitenwechsel: Rotes Team → BlueSpawn, Blaues Team → RedSpawn
for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getBlueSpawn()); } for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getBlueSpawn()); }
@@ -472,6 +489,10 @@ public class Game {
cd--; cd--;
} else { } else {
spawnBallDelayed(arena.getBallSpawn()); spawnBallDelayed(arena.getBallSpawn());
// Regel 8: Das andere Team (Blau) stößt in der 2. Halbzeit an
throwInTeam = Team.BLUE;
kickoffEnforceTicks = 200;
broadcastAll(Messages.get("kickoff-team", "team", "§9Blaues Team"));
for (UUID uuid : allPlayers) { for (UUID uuid : allPlayers) {
Player p = Bukkit.getPlayer(uuid); Player p = Bukkit.getPlayer(uuid);
if (p != null) { if (p != null) {
@@ -497,11 +518,16 @@ public class Game {
if (ball != null) ball.remove(); if (ball != null) ball.remove();
spawnBallDelayed(arena.getBallSpawn()); spawnBallDelayed(arena.getBallSpawn());
// Regel 8: In der Verlängerung stößt das Team an, das in der 2. Halbzeit NICHT angestoßen hat.
// In der 2. HZ stieß Blau an → in der VL stößt Rot an.
throwInTeam = Team.RED;
kickoffEnforceTicks = 200; // 10s Anstoß-Kreis
broadcastAll("§6§l╔══════════════════════╗"); broadcastAll("§6§l╔══════════════════════╗");
broadcastAll("§6§l║ ⚽ VERLÄNGERUNG! ║"); broadcastAll("§6§l║ ⚽ VERLÄNGERUNG! ║");
broadcastAll("§6§l╚══════════════════════╝"); broadcastAll("§6§l╚══════════════════════╝");
broadcastAll("§7Spielstand: §c" + redScore + " §7: §9" + blueScore); broadcastAll("§7Spielstand: §c" + redScore + " §7: §9" + blueScore);
broadcastAll(Messages.get("kickoff-team", "team", "§cRotes Team"));
for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getRedSpawn()); p.sendTitle("§6§lVERLÄNGERUNG!", "§710 Minuten extra!", 10, 60, 10); } } for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getRedSpawn()); p.sendTitle("§6§lVERLÄNGERUNG!", "§710 Minuten extra!", 10, 60, 10); } }
for (UUID uuid : blueTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getBlueSpawn()); p.sendTitle("§6§lVERLÄNGERUNG!", "§710 Minuten extra!", 10, 60, 10); } } for (UUID uuid : blueTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getBlueSpawn()); p.sendTitle("§6§lVERLÄNGERUNG!", "§710 Minuten extra!", 10, 60, 10); } }
@@ -856,17 +882,27 @@ public class Game {
Vector diff = to.toVector().subtract(from.toVector()); Vector diff = to.toVector().subtract(from.toVector());
double distance = diff.length(); double distance = diff.length();
int steps = Math.max(1, (int) Math.ceil(distance / 0.2)); int steps = Math.max(1, (int) Math.ceil(distance / 0.2));
// BUG FIX: Vier Y-Offsets prüfen:
// 0.0 = ArmorStand-Füße (Entity-Position)
// 0.5 = Mitte des kleinen Stands
// 0.975 = tatsächliche Helmposition (Textur sichtbar hier!)
// 1.4 = konservativer oberer Puffer
// Früher wurden nur 0 und 1.4 geprüft → Bälle auf Helm-Höhe (0.975)
// wurden nicht als Tor erkannt und landeten als Ecke/Abstoß.
final double[] Y_OFFSETS = {0.0, 0.5, 0.975, 1.4};
for (int i = 0; i <= steps; i++) { for (int i = 0; i <= steps; i++) {
double t = (double) i / steps; double t = (double) i / steps;
Location p = from.clone().add(diff.clone().multiply(t)); Location base = from.clone().add(diff.clone().multiply(t));
Location head = p.clone().add(0, 1.4, 0); for (double dy : Y_OFFSETS) {
// In der 2. Halbzeit sind die Seiten getauscht → Tore umkehren Location check = base.clone().add(0, dy, 0);
if (!secondHalf) { // In der 2. Halbzeit sind die Seiten getauscht → Tore umkehren
if (arena.isInRedGoal(p) || arena.isInRedGoal(head)) return Team.BLUE; if (!secondHalf) {
if (arena.isInBlueGoal(p) || arena.isInBlueGoal(head)) return Team.RED; if (arena.isInRedGoal(check)) return Team.BLUE;
} else { if (arena.isInBlueGoal(check)) return Team.RED;
if (arena.isInRedGoal(p) || arena.isInRedGoal(head)) return Team.RED; } else {
if (arena.isInBlueGoal(p) || arena.isInBlueGoal(head)) return Team.BLUE; if (arena.isInRedGoal(check)) return Team.RED;
if (arena.isInBlueGoal(check)) return Team.BLUE;
}
} }
} }
return null; return null;
@@ -910,25 +946,47 @@ public class Game {
} }
} }
case "redEnd" -> { case "redEnd" -> {
if (touchTeam == Team.RED) { // Korrekte Fußball-Regel:
resumeLocation = moveInsideField(getCornerLocation(outLocation), 1.25); // Letzter Kontakt durch VERTEIDIGER an eigener Torlinie → ECKE für Angreifer
throwInTeam = Team.BLUE; // Letzter Kontakt durch ANGREIFER an gegnerischer Torlinie → ABSTOSS für Verteidiger
message = "§e⚽ §7Ball im Aus! §9Ecke für Blaues Team§7!"; // 1. Halbzeit: ROT verteidigt diese Seite (redEnd = rotes Tor)
// 2. Halbzeit: BLAU verteidigt diese Seite (Seitenwechsel)
Team defenderHere = secondHalf ? Team.BLUE : Team.RED;
Team attackerHere = defenderHere.getOpponent();
if (touchTeam == defenderHere) {
// Verteidiger hat den Ball ins Aus geschossen → ECKE für Angreifer
// Ball an Strafraumgrenze (11m-Linie) platzieren nicht in der Spielfeldecke
resumeLocation = moveInsideField(getPenaltyAreaCornerLocation(outLocation, true), 1.25);
throwInTeam = attackerHere;
String teamStr = attackerHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
message = "§e⚽ §7Ball im Aus! §7Ecke für " + teamStr + "§7!";
} else { } else {
resumeLocation = moveInsideField(arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn(), 1.25); // Angreifer (oder unbekannt) hat den Ball ins Aus geschossen → ABSTOSS für Verteidiger
throwInTeam = Team.RED; // Ball ~5,5 Blöcke vor der Torlinie (5-Meter-Raum), Feldmitte
message = "§e⚽ §7Ball im Aus! §cAbstoß für Rotes Team§7!"; resumeLocation = moveInsideField(getGoalKickSpawnLocation(true), 1.25);
throwInTeam = defenderHere;
String teamStr = defenderHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
message = "§e⚽ §7Ball im Aus! §7Abstoß für " + teamStr + "§7!";
} }
} }
case "blueEnd" -> { case "blueEnd" -> {
if (touchTeam == Team.BLUE) { // analog zu redEnd.
resumeLocation = moveInsideField(getCornerLocation(outLocation), 1.25); // 1. Halbzeit: BLAU verteidigt diese Seite (blueEnd = blaues Tor)
throwInTeam = Team.RED; // 2. Halbzeit: ROT verteidigt diese Seite (Seitenwechsel)
message = "§e⚽ §7Ball im Aus! §cEcke für Rotes Team§7!"; Team defenderHere = secondHalf ? Team.RED : Team.BLUE;
Team attackerHere = defenderHere.getOpponent();
if (touchTeam == defenderHere) {
// Verteidiger → ECKE für Angreifer
resumeLocation = moveInsideField(getPenaltyAreaCornerLocation(outLocation, false), 1.25);
throwInTeam = attackerHere;
String teamStr = attackerHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
message = "§e⚽ §7Ball im Aus! §7Ecke für " + teamStr + "§7!";
} else { } else {
resumeLocation = moveInsideField(arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn(), 1.25); // Angreifer (oder unbekannt) → ABSTOSS für Verteidiger
throwInTeam = Team.BLUE; resumeLocation = moveInsideField(getGoalKickSpawnLocation(false), 1.25);
message = "§e⚽ §7Ball im Aus! §9Abstoß für Blaues Team§7!"; throwInTeam = defenderHere;
String teamStr = defenderHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
message = "§e⚽ §7Ball im Aus! §7Abstoß für " + teamStr + "§7!";
} }
} }
default -> { default -> {
@@ -942,14 +1000,137 @@ public class Game {
for (UUID uuid : allPlayers) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 1f, 1.2f); } for (UUID uuid : allPlayers) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 1f, 1.2f); }
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-out", 3)); addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-out", 3));
// throwInTeam sichern spawnBallDelayed() würde es auf null zurücksetzen
final Team capturedThrowIn = throwInTeam;
final Location spawnHere = resumeLocation; final Location spawnHere = resumeLocation;
new BukkitRunnable() { new BukkitRunnable() {
public void run() { public void run() {
if (state == GameState.RUNNING || state == GameState.OVERTIME) spawnBallDelayed(spawnHere); if (state == GameState.RUNNING || state == GameState.OVERTIME) {
spawnBallDelayed(spawnHere);
throwInTeam = capturedThrowIn;
// Abstandsregel pro Spielfortsetzungs-Typ erzwingen (Regel 15/16/17)
freekickLocation = spawnHere.clone();
freekickTicks = plugin.getConfig().getInt("gameplay.freekick-duration", 600);
}
} }
}.runTaskLater(plugin, 40L); }.runTaskLater(plugin, 40L);
} }
/**
* Berechnet den Freistoß-Aufstellungsort für eine Ecke.
* Statt der wörtlichen Spielfeldecke wird der Ball an der Strafraumgrenze
* (Tiefe aus gameplay.penalty-area-depth) entlang der nächsten Seitenlinie platziert.
* Das entspricht dem Wunsch "an oder vor der 11-Meter-Grenze".
*
* @param outLoc Wo der Ball das Feld verlassen hat
* @param isRedEnd true = rotes Tor-Ende (redGoal-Seite des Feldes)
*/
private Location getPenaltyAreaCornerLocation(Location outLoc, boolean isRedEnd) {
if (arena.getFieldMin() == null || arena.getFieldMax() == null || arena.getBallSpawn() == null) {
return getCornerLocation(outLoc);
}
double y = arena.getBallSpawn().getY();
double penaltyDepth = plugin.getConfig().getDouble("gameplay.penalty-area-depth", 16);
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
double maxX = Math.max(arena.getFieldMin().getX(), arena.getFieldMax().getX());
double minZ = Math.min(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
double maxZ = Math.max(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
org.bukkit.util.Vector fieldDir = arena.getFieldDirection();
if (fieldDir == null) return getCornerLocation(outLoc);
if (Math.abs(fieldDir.getZ()) >= Math.abs(fieldDir.getX())) {
// ── Feld läuft entlang Z-Achse ──────────────────────────────────
boolean redIsLowZ = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
// Nächste Seitenlinie (X-Seite) bestimmen
double sideX = (Math.abs(outLoc.getX() - minX) <= Math.abs(outLoc.getX() - maxX)) ? minX : maxX;
// Z-Position: Strafraum-Tiefe vom Toraus-Ende ins Feld
double endZ, targetZ;
if (isRedEnd) {
endZ = redIsLowZ ? minZ : maxZ;
targetZ = redIsLowZ ? endZ + penaltyDepth : endZ - penaltyDepth;
} else {
endZ = redIsLowZ ? maxZ : minZ;
targetZ = redIsLowZ ? endZ - penaltyDepth : endZ + penaltyDepth;
}
targetZ = Math.max(minZ + 1.0, Math.min(maxZ - 1.0, targetZ));
return new Location(outLoc.getWorld(), sideX, y, targetZ);
} else {
// ── Feld läuft entlang X-Achse ──────────────────────────────────
boolean redIsLowX = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
double sideZ = (Math.abs(outLoc.getZ() - minZ) <= Math.abs(outLoc.getZ() - maxZ)) ? minZ : maxZ;
double endX, targetX;
if (isRedEnd) {
endX = redIsLowX ? minX : maxX;
targetX = redIsLowX ? endX + penaltyDepth : endX - penaltyDepth;
} else {
endX = redIsLowX ? maxX : minX;
targetX = redIsLowX ? endX - penaltyDepth : endX + penaltyDepth;
}
targetX = Math.max(minX + 1.0, Math.min(maxX - 1.0, targetX));
return new Location(outLoc.getWorld(), targetX, y, sideZ);
}
}
/**
* Gibt den Ball-Aufstellungsort für einen Abstoß zurück.
* Der Ball wird ~5.5 Blöcke vor der Torlinie, mittig auf dem Feld platziert
* (entspricht dem 5-Meter-Raum / Torabstoß-Raum im echten Fußball).
*
* @param isRedEnd true = roter Torbereich
*/
private Location getGoalKickSpawnLocation(boolean isRedEnd) {
if (arena.getFieldMin() == null || arena.getFieldMax() == null || arena.getBallSpawn() == null) {
if (isRedEnd) return arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn();
else return arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn();
}
double y = arena.getBallSpawn().getY();
final double GOAL_KICK_INSET = 5.5; // ~5-Meter-Raum (6-Yard-Box)
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
double maxX = Math.max(arena.getFieldMin().getX(), arena.getFieldMax().getX());
double minZ = Math.min(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
double maxZ = Math.max(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
double centerX = (minX + maxX) / 2.0;
double centerZ = (minZ + maxZ) / 2.0;
org.bukkit.util.Vector fieldDir = arena.getFieldDirection();
if (fieldDir == null) {
return isRedEnd ? (arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn())
: (arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn());
}
if (Math.abs(fieldDir.getZ()) >= Math.abs(fieldDir.getX())) {
// Feld entlang Z-Achse
boolean redIsLowZ = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
double kickZ;
if (isRedEnd) {
double endZ = redIsLowZ ? minZ : maxZ;
kickZ = redIsLowZ ? endZ + GOAL_KICK_INSET : endZ - GOAL_KICK_INSET;
} else {
double endZ = redIsLowZ ? maxZ : minZ;
kickZ = redIsLowZ ? endZ - GOAL_KICK_INSET : endZ + GOAL_KICK_INSET;
}
kickZ = Math.max(minZ + 1.0, Math.min(maxZ - 1.0, kickZ));
return new Location(arena.getFieldMin().getWorld(), centerX, y, kickZ);
} else {
// Feld entlang X-Achse
boolean redIsLowX = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
double kickX;
if (isRedEnd) {
double endX = redIsLowX ? minX : maxX;
kickX = redIsLowX ? endX + GOAL_KICK_INSET : endX - GOAL_KICK_INSET;
} else {
double endX = redIsLowX ? maxX : minX;
kickX = redIsLowX ? endX - GOAL_KICK_INSET : endX + GOAL_KICK_INSET;
}
kickX = Math.max(minX + 1.0, Math.min(maxX - 1.0, kickX));
return new Location(arena.getFieldMin().getWorld(), kickX, y, centerZ);
}
}
/** Hilfsmethode: wörtliche Spielfeldecke (als Fallback). */
private Location getCornerLocation(Location outLoc) { private Location getCornerLocation(Location outLoc) {
if (arena.getFieldMin() == null || arena.getFieldMax() == null) return arena.getBallSpawn(); if (arena.getFieldMin() == null || arena.getFieldMax() == null) return arena.getBallSpawn();
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX()); double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
@@ -1019,8 +1200,8 @@ public class Game {
} }
} }
throwInTeam = null; clearThrowIn(); // setzt lastKickWasRestart=true falls Einwurf/Restart war → kein Abseits für Empfänger (Regel 11 §3)
setLastKicker(uuid); // korrekt: nutzt setLastKicker statt direktem Feldzugriff setLastKicker(uuid);
ball.kick(p); ball.kick(p);
break; // pro Tick max. 1 Auto-Kick break; // pro Tick max. 1 Auto-Kick
} }
@@ -1370,30 +1551,43 @@ public class Game {
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-foul", 5)); addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-foul", 5));
logMatchEvent("§cFoul: §e" + fouler.getName() + " §7→ §e" + victim.getName()); logMatchEvent("§cFoul: §e" + fouler.getName() + " §7→ §e" + victim.getName());
// ── Foul im Strafraum → Elfmeter ─────────────────────────────────── // ── Foul im Strafraum → Elfmeter (Regel 14) ──────────────────────────────
// Regel 14: Strafstoß wenn ein Spieler ein direktes Foul im eigenen Strafraum begeht.
// In der 2. Halbzeit sind die Seiten getauscht:
// 1. HZ: Rot verteidigt roten SR, Blau verteidigt blauen SR
// 2. HZ: Blau verteidigt roten SR, Rot verteidigt blauen SR
boolean inRedPenalty = arena.isInRedPenaltyArea(foulLocation); boolean inRedPenalty = arena.isInRedPenaltyArea(foulLocation);
boolean inBluePenalty = arena.isInBluePenaltyArea(foulLocation); boolean inBluePenalty = arena.isInBluePenaltyArea(foulLocation);
boolean penaltyKick = false; boolean penaltyKick = false;
if (inRedPenalty && victimTeam == Team.BLUE) { boolean penaltyForBlue, penaltyForRed;
// Foul an Blau im roten Strafraum → Elfmeter für Blau if (!secondHalf) {
penaltyForBlue = inRedPenalty && victimTeam == Team.BLUE;
penaltyForRed = inBluePenalty && victimTeam == Team.RED;
} else {
// Seitenwechsel: Blau greift jetzt auf roten SR-Seite an
penaltyForBlue = inBluePenalty && victimTeam == Team.BLUE;
penaltyForRed = inRedPenalty && victimTeam == Team.RED;
}
if (penaltyForBlue) {
broadcastAll(Messages.get("foul-penalty", "team", "§9Blaues Team")); broadcastAll(Messages.get("foul-penalty", "team", "§9Blaues Team"));
for (UUID uuid : getAllAndSpectators()) { for (UUID uuid : getAllAndSpectators()) {
Player p = Bukkit.getPlayer(uuid); Player p = Bukkit.getPlayer(uuid);
if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§9Blaues Team§7 schießt!", 5, 50, 10); if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§9Blaues Team§7 schießt!", 5, 50, 10);
} }
penaltyKick = true; penaltyKick = true;
// Elfmeter als Freistoß direkt auf Ballspawn (ggf. später: separater Elfmeter-Punkt) Location penSpot = arena.getPenaltySpot(Team.BLUE);
startFreekick(Team.BLUE, arena.getBallSpawn(), "Elfmeter"); startFreekick(Team.BLUE, penSpot != null ? penSpot : arena.getBallSpawn(), "Elfmeter");
} else if (inBluePenalty && victimTeam == Team.RED) { } else if (penaltyForRed) {
// Foul an Rot im blauen Strafraum → Elfmeter für Rot
broadcastAll(Messages.get("foul-penalty", "team", "§cRotes Team")); broadcastAll(Messages.get("foul-penalty", "team", "§cRotes Team"));
for (UUID uuid : getAllAndSpectators()) { for (UUID uuid : getAllAndSpectators()) {
Player p = Bukkit.getPlayer(uuid); Player p = Bukkit.getPlayer(uuid);
if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§cRotes Team§7 schießt!", 5, 50, 10); if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§cRotes Team§7 schießt!", 5, 50, 10);
} }
penaltyKick = true; penaltyKick = true;
startFreekick(Team.RED, arena.getBallSpawn(), "Elfmeter"); Location penSpot = arena.getPenaltySpot(Team.RED);
startFreekick(Team.RED, penSpot != null ? penSpot : arena.getBallSpawn(), "Elfmeter");
} }
if (!penaltyKick) { if (!penaltyKick) {
@@ -1447,8 +1641,18 @@ public class Game {
broadcastAll(Messages.get("freekick-hint", "n", String.format("%.0f", dist))); broadcastAll(Messages.get("freekick-hint", "n", String.format("%.0f", dist)));
} }
/**
* Erzwingt den korrekten Abstand für die jeweilige Spielfortsetzung (Regel 1317).
* Freistoß (Regel 13): 5 Blöcke (config: freekick-distance)
* Einwurf (Regel 15): 2 Blöcke
* Abstoß (Regel 16): 9,15 Blöcke (Gegner außerhalb Strafraum)
* Eckstoß (Regel 17): 9,15 Blöcke
* Anstoß (Regel 8): 9,15 Blöcke (via kickoffEnforceTicks)
*/
private void enforceFreekickDistance() { private void enforceFreekickDistance() {
if (freekickLocation == null || throwInTeam == null) return; if (freekickLocation == null || throwInTeam == null) return;
// Abstand je nach Typ: prüfe ob freekickLocation nah an einer Seitenlinie ist (=Einwurf)
// oder in der Feldhälfte nahe einer Torlinie (=Abstoß/Eckstoß) oder zentral (=Freistoß)
double minDist = plugin.getConfig().getDouble("gameplay.freekick-distance", 5.0); double minDist = plugin.getConfig().getDouble("gameplay.freekick-distance", 5.0);
Team opposingTeam = throwInTeam.getOpponent(); Team opposingTeam = throwInTeam.getOpponent();
List<UUID> opponents = opposingTeam == Team.RED ? redTeam : blueTeam; List<UUID> opponents = opposingTeam == Team.RED ? redTeam : blueTeam;
@@ -1840,10 +2044,11 @@ public class Game {
throwInTeam = null; throwInTeam = null;
injuryTimeBuffer = 0; injuryTimeBuffer = 0;
inInjuryTime = false; inInjuryTime = false;
lastKicker = null; lastKicker = null;
secondLastKicker = null; secondLastKicker = null;
lastKickWasHeader = false; lastKickWasHeader = false;
secondHalf = false; lastKickWasRestart = false;
secondHalf = false;
updateGoalBeaconColors(); updateGoalBeaconColors();
// Persistente Statistiken speichern // Persistente Statistiken speichern
@@ -2121,7 +2326,8 @@ public class Game {
Player newKicker = Bukkit.getPlayer(uuid); Player newKicker = Bukkit.getPlayer(uuid);
if (prevKicker != null && newKicker != null) { if (prevKicker != null && newKicker != null) {
double dist = lastKickLocation.distance(ball.getEntity().getLocation()); double dist = lastKickLocation.distance(ball.getEntity().getLocation());
if (dist >= LONG_PASS_DISTANCE && getTeam(prevKicker) == getTeam(newKicker)) { double longPassDist = plugin.getConfig().getDouble("gameplay.long-pass-distance", LONG_PASS_DISTANCE);
if (dist >= longPassDist && getTeam(prevKicker) == getTeam(newKicker)) {
// Langer Pass innerhalb des Teams // Langer Pass innerhalb des Teams
String msg = "§7⚽ §eLangpass §7von §f" + prevKicker.getName() String msg = "§7⚽ §eLangpass §7von §f" + prevKicker.getName()
+ " §7zu §f" + newKicker.getName() + " §7zu §f" + newKicker.getName()
@@ -2144,10 +2350,16 @@ public class Game {
if (p != null) lastTouchTeam = getTeam(p); if (p != null) lastTouchTeam = getTeam(p);
if (p != null) kicks.merge(uuid, 1, Integer::sum); if (p != null) kicks.merge(uuid, 1, Integer::sum);
// Abseits-Check // ── Abseits-Check (Regel 11 §3: kein Abseits nach Einwurf, Abstoß, Eckstoß) ──
// lastKickWasRestart wird in clearThrowIn() gesetzt wenn throwInTeam != null war.
// Der EMPFÄNGER des ersten Restart-Passes darf nicht auf Abseits geprüft werden.
// Das Flag wird hier konsumiert (→ gilt nur für diesen einen Empfänger).
boolean skipOffside = lastKickWasRestart;
lastKickWasRestart = false; // Flag zurücksetzen nach Konsum
if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true) if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true)
&& (state == GameState.RUNNING || state == GameState.OVERTIME) && (state == GameState.RUNNING || state == GameState.OVERTIME)
&& ball != null && ball.getEntity() != null && !offsideCooldown) { && ball != null && ball.getEntity() != null && !offsideCooldown
&& !skipOffside) {
checkOffside(uuid, ball.getEntity().getLocation()); checkOffside(uuid, ball.getEntity().getLocation());
} }
} }
@@ -2163,9 +2375,13 @@ public class Game {
if (p != null) lastTouchTeam = getTeam(p); if (p != null) lastTouchTeam = getTeam(p);
if (p != null) kicks.merge(uuid, 1, Integer::sum); if (p != null) kicks.merge(uuid, 1, Integer::sum);
// Kopfball: gleiche Abseits-Logik kein Abseits wenn Restart-Empfänger
boolean skipOffside = lastKickWasRestart;
lastKickWasRestart = false;
if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true) if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true)
&& (state == GameState.RUNNING || state == GameState.OVERTIME) && (state == GameState.RUNNING || state == GameState.OVERTIME)
&& ball != null && ball.getEntity() != null && !offsideCooldown) { && ball != null && ball.getEntity() != null && !offsideCooldown
&& !skipOffside) {
checkOffside(uuid, ball.getEntity().getLocation()); checkOffside(uuid, ball.getEntity().getLocation());
} }
} }
@@ -2201,6 +2417,8 @@ public class Game {
public boolean isLastKickWasHeader() { return lastKickWasHeader; } public boolean isLastKickWasHeader() { return lastKickWasHeader; }
/** Berechtigung aufheben wird von BallListener nach dem ersten Schuss gerufen */ /** Berechtigung aufheben wird von BallListener nach dem ersten Schuss gerufen */
public void clearThrowIn() { public void clearThrowIn() {
// Wenn throwInTeam gesetzt war, war das ein Restart-Kick → nächster Empfänger kein Abseits
if (throwInTeam != null) lastKickWasRestart = true;
throwInTeam = null; throwInTeam = null;
freekickLocation = null; freekickLocation = null;
freekickTicks = 0; freekickTicks = 0;

View File

@@ -4,6 +4,7 @@ import de.fussball.plugin.Fussball;
import de.fussball.plugin.game.Game; import de.fussball.plugin.game.Game;
import de.fussball.plugin.game.GameState; import de.fussball.plugin.game.GameState;
import de.fussball.plugin.stats.StatsManager; import de.fussball.plugin.stats.StatsManager;
import org.bukkit.ChatColor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Color; import org.bukkit.Color;
import org.bukkit.Location; import org.bukkit.Location;
@@ -15,6 +16,10 @@ import org.bukkit.util.Transformation;
import org.joml.AxisAngle4f; import org.joml.AxisAngle4f;
import org.joml.Vector3f; import org.joml.Vector3f;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -44,14 +49,22 @@ public class FussballHologram {
private final Map<UUID, Interaction> playerInteractions = new ConcurrentHashMap<>(); private final Map<UUID, Interaction> playerInteractions = new ConcurrentHashMap<>();
/** Aktuell angezeigte Seite (0 = GOALS, 1 = WINS) pro Spieler */ /** Aktuell angezeigte Seite (0 = GOALS, 1 = WINS) pro Spieler */
private final Map<UUID, Integer> currentPage = new ConcurrentHashMap<>(); private final Map<UUID, Integer> currentPage = new ConcurrentHashMap<>();
private final Map<HoloType, List<String>> customTextTemplates = new EnumMap<>(HoloType.class);
private final Fussball plugin; private final Fussball plugin;
public FussballHologram(String id, Location location, HoloType type, Fussball plugin) { public FussballHologram(String id, Location location, HoloType type, Fussball plugin) {
this(id, location, type, plugin, Collections.emptyMap());
}
public FussballHologram(String id, Location location, HoloType type, Fussball plugin, Map<HoloType, List<String>> customTextTemplates) {
this.id = id; this.id = id;
this.location = location.clone(); this.location = location.clone();
this.type = type; this.type = type;
this.plugin = plugin; this.plugin = plugin;
for (Map.Entry<HoloType, List<String>> entry : customTextTemplates.entrySet()) {
setCustomText(entry.getKey(), entry.getValue());
}
} }
// ── Seiten-Wechsel ─────────────────────────────────────────────────────── // ── Seiten-Wechsel ───────────────────────────────────────────────────────
@@ -183,70 +196,228 @@ public class FussballHologram {
.anyMatch(i -> i.getUniqueId().equals(entityId)); .anyMatch(i -> i.getUniqueId().equals(entityId));
} }
public void setCustomText(HoloType targetType, List<String> lines) {
if (lines == null || lines.isEmpty()) {
customTextTemplates.remove(targetType);
return;
}
List<String> sanitized = new ArrayList<>();
for (String line : lines) {
sanitized.add(ChatColor.translateAlternateColorCodes('&', line));
}
customTextTemplates.put(targetType, List.copyOf(sanitized));
}
public void clearCustomText(HoloType targetType) {
customTextTemplates.remove(targetType);
}
public List<String> getCustomText(HoloType targetType) {
return customTextTemplates.getOrDefault(targetType, Collections.emptyList());
}
/** Baut den anzuzeigenden Text aus den aktuellen Top-10-Statistiken */ /** Baut den anzuzeigenden Text aus den aktuellen Top-10-Statistiken */
private String buildText(HoloType showType) { private String buildText(HoloType showType) {
if (showType == HoloType.MATCH) { if (showType == HoloType.MATCH) {
return buildMatchText(); return buildMatchText();
} }
StringBuilder sb = new StringBuilder(); String customText = buildCustomStatsText(showType);
if (customText != null) {
return customText;
}
return buildDefaultStatsText(showType);
}
private String buildDefaultStatsText(HoloType showType) {
String nameColor = holoColor("name-color", "&0");
String labelColor = holoColor("label-color", "&8");
String sep = holoColor("separator-color", "&8&m") + "══════════════════════" + ChatColor.RESET;
StringBuilder sb = new StringBuilder();
if (showType == HoloType.GOALS) { if (showType == HoloType.GOALS) {
sb.append("§6§l⚽ TOP 10 TORSCHÜTZEN ⚽\n"); String title = holoColor("goals-title-color", "&6&l") + "⚽ TOP 10 TORSCHÜTZEN ⚽";
sb.append("§8§m══════════════════════§r\n"); String valColor = holoColor("goals-value-color", "&4");
String toggle = holoColor("toggle-color", "&8&o") + "[Rechtsklick → Siege anzeigen]";
sb.append(title).append("\n");
sb.append(sep).append("\n");
var list = plugin.getStatsManager().getTopScorers(10); var list = plugin.getStatsManager().getTopScorers(10);
if (list.isEmpty()) { if (list.isEmpty()) {
sb.append("§8Noch keine Statistiken vorhanden."); sb.append(labelColor).append("Noch keine Statistiken vorhanden.");
} else { } else {
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
StatsManager.PlayerStats s = list.get(i).getValue(); StatsManager.PlayerStats s = list.get(i).getValue();
sb.append(medal(i + 1)) sb.append(medal(i + 1))
.append(" §0").append(s.name) .append(" ").append(nameColor).append(s.name)
.append(" §4").append(s.goals).append(" §8Tore"); .append(" ").append(valColor).append(s.goals).append(" ").append(labelColor).append("Tore");
if (i < list.size() - 1) sb.append("\n"); if (i < list.size() - 1) sb.append("\n");
} }
} }
sb.append("\n§8§m══════════════════════§r"); sb.append("\n").append(sep);
sb.append("\n§8§o[Rechtsklick → Siege anzeigen]"); sb.append("\n").append(toggle);
} else { } else {
sb.append("§2§l🏆 TOP 10 GEWINNER 🏆\n"); String title = holoColor("wins-title-color", "&2&l") + "🏆 TOP 10 GEWINNER 🏆";
sb.append("§8§m══════════════════════§r\n"); String valColor = holoColor("wins-value-color", "&2");
String toggle = holoColor("toggle-color", "&8&o") + "[Rechtsklick → Tore anzeigen]";
sb.append(title).append("\n");
sb.append(sep).append("\n");
var list = plugin.getStatsManager().getTopWins(10); var list = plugin.getStatsManager().getTopWins(10);
if (list.isEmpty()) { if (list.isEmpty()) {
sb.append("§8Noch keine Statistiken vorhanden."); sb.append(labelColor).append("Noch keine Statistiken vorhanden.");
} else { } else {
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
StatsManager.PlayerStats s = list.get(i).getValue(); StatsManager.PlayerStats s = list.get(i).getValue();
sb.append(medal(i + 1)) sb.append(medal(i + 1))
.append(" §0").append(s.name) .append(" ").append(nameColor).append(s.name)
.append(" §2").append(s.wins).append(" §8Siege") .append(" ").append(valColor).append(s.wins).append(" ").append(labelColor).append("Siege")
.append(" §8(").append(String.format("%.0f", s.getWinRate())).append("%)"); .append(" ").append(labelColor).append("(").append(String.format("%.0f", s.getWinRate())).append("%)");
if (i < list.size() - 1) sb.append("\n"); if (i < list.size() - 1) sb.append("\n");
} }
} }
sb.append("\n§8§m══════════════════════§r"); sb.append("\n").append(sep);
sb.append("\n§8§o[Rechtsklick → Tore anzeigen]"); sb.append("\n").append(toggle);
} }
return sb.toString(); return sb.toString();
} }
private String buildMatchText() { private String buildCustomStatsText(HoloType showType) {
Game game = findRelevantGame(); List<String> template = customTextTemplates.get(showType);
if (game == null) { if (template == null || template.isEmpty()) {
return "§e§lLive Match\n§8§m────────────────\n§7Kein Spiel aktiv\n§8- : -\n§8--:--"; return null;
} }
String header = "§e§lLive Match"; String sep = holoColor("separator-color", "&8&m") + "══════════════════════" + ChatColor.RESET;
String separator = "§8§m────────────────"; String title, toggle;
String phase = buildPhaseLabel(game); if (showType == HoloType.GOALS) {
String score = "§c§l" + game.getRedScore() + " §r§7: §9§l" + game.getBlueScore(); title = holoColor("goals-title-color", "&6&l") + "⚽ TOP 10 TORSCHÜTZEN ⚽";
String timeLabel = game.isInInjuryTime() toggle = holoColor("toggle-color", "&8&o") + "[Rechtsklick → Siege anzeigen]";
? "§c+" + formatInjury(game.getInjuryTimeBuffer()) + " §7(Nachspielzeit)" } else {
: "§e" + formatMainTime(game.getTimeLeft()); title = holoColor("wins-title-color", "&2&l") + "🏆 TOP 10 GEWINNER 🏆";
toggle = holoColor("toggle-color", "&8&o") + "[Rechtsklick → Tore anzeigen]";
}
String entries = buildStatsEntries(showType);
return header + "\n" + separator + "\n" + phase + "\n" + score + "\n" + timeLabel; StringBuilder sb = new StringBuilder();
for (int i = 0; i < template.size(); i++) {
String line = template.get(i)
.replace("{title}", title)
.replace("{separator}", sep)
.replace("{entries}", entries)
.replace("{toggle}", toggle);
sb.append(line);
if (i < template.size() - 1) {
sb.append("\n");
}
}
return sb.toString();
}
private String buildStatsEntries(HoloType showType) {
String nameColor = holoColor("name-color", "&0");
String labelColor = holoColor("label-color", "&8");
StringBuilder entries = new StringBuilder();
if (showType == HoloType.GOALS) {
String valColor = holoColor("goals-value-color", "&4");
var list = plugin.getStatsManager().getTopScorers(10);
if (list.isEmpty()) {
return labelColor + "Noch keine Statistiken vorhanden.";
}
for (int i = 0; i < list.size(); i++) {
StatsManager.PlayerStats s = list.get(i).getValue();
entries.append(medal(i + 1))
.append(" ").append(nameColor).append(s.name)
.append(" ").append(valColor).append(s.goals).append(" ").append(labelColor).append("Tore");
if (i < list.size() - 1) {
entries.append("\n");
}
}
return entries.toString();
}
String valColor = holoColor("wins-value-color", "&2");
var list = plugin.getStatsManager().getTopWins(10);
if (list.isEmpty()) {
return labelColor + "Noch keine Statistiken vorhanden.";
}
for (int i = 0; i < list.size(); i++) {
StatsManager.PlayerStats s = list.get(i).getValue();
entries.append(medal(i + 1))
.append(" ").append(nameColor).append(s.name)
.append(" ").append(valColor).append(s.wins).append(" ").append(labelColor).append("Siege")
.append(" ").append(labelColor).append("(").append(String.format("%.0f", s.getWinRate())).append("%)");
if (i < list.size() - 1) {
entries.append("\n");
}
}
return entries.toString();
}
private String buildMatchText() {
String customText = buildCustomMatchText();
if (customText != null) {
return customText;
}
Game game = findRelevantGame();
String header = holoColor("match-header-color", "&e&l") + "Live Match";
String sep = holoColor("separator-color", "&8&m") + "────────────────";
if (game == null) {
String lc = holoColor("label-color", "&8");
return header + "\n" + sep + "\n§7Kein Spiel aktiv\n" + lc + "- : -\n" + lc + "--:--";
}
String phase = buildPhaseLabel(game);
String score = holoColor("match-score-red", "&c&l") + game.getRedScore()
+ " §r§7: "
+ holoColor("match-score-blue", "&9&l") + game.getBlueScore();
String timeLabel = game.isInInjuryTime()
? holoColor("match-injury-color", "&c") + "+" + formatInjury(game.getInjuryTimeBuffer()) + " §7(Nachspielzeit)"
: holoColor("match-time-color", "&e") + formatMainTime(game.getTimeLeft());
return header + "\n" + sep + "\n" + phase + "\n" + score + "\n" + timeLabel;
}
private String buildCustomMatchText() {
List<String> template = customTextTemplates.get(HoloType.MATCH);
if (template == null || template.isEmpty()) {
return null;
}
Game game = findRelevantGame();
String header = holoColor("match-header-color", "&e&l") + "Live Match";
String separator = holoColor("separator-color", "&8&m") + "────────────────";
String phase = game == null ? "§7Kein Spiel aktiv" : buildPhaseLabel(game);
String score, time;
if (game == null) {
String lc = holoColor("label-color", "&8");
score = lc + "- : -";
time = lc + "--:--";
} else {
score = holoColor("match-score-red", "&c&l") + game.getRedScore()
+ " §r§7: "
+ holoColor("match-score-blue", "&9&l") + game.getBlueScore();
time = game.isInInjuryTime()
? holoColor("match-injury-color", "&c") + "+" + formatInjury(game.getInjuryTimeBuffer()) + " §7(Nachspielzeit)"
: holoColor("match-time-color", "&e") + formatMainTime(game.getTimeLeft());
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < template.size(); i++) {
String line = template.get(i)
.replace("{header}", header)
.replace("{separator}", separator)
.replace("{phase}", phase)
.replace("{score}", score)
.replace("{time}", time);
sb.append(line);
if (i < template.size() - 1) {
sb.append("\n");
}
}
return sb.toString();
} }
private String buildPhaseLabel(Game game) { private String buildPhaseLabel(Game game) {
@@ -317,6 +488,11 @@ public class FussballHologram {
return "+" + safe + "s"; return "+" + safe + "s";
} }
private String holoColor(String key, String def) {
String val = plugin.getConfig().getString("holograms." + key, def);
return ChatColor.translateAlternateColorCodes('&', val != null ? val : def);
}
private String medal(int rank) { private String medal(int rank) {
return switch (rank) { return switch (rank) {
case 1 -> "§6§l#1"; // Gold bleibt hebt sich gut ab case 1 -> "§6§l#1"; // Gold bleibt hebt sich gut ab

View File

@@ -21,6 +21,10 @@ import org.bukkit.scheduler.BukkitTask;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -115,7 +119,7 @@ public class HologramManager implements Listener {
type = FussballHologram.HoloType.GOALS; type = FussballHologram.HoloType.GOALS;
} }
holograms.put(id, new FussballHologram(id, loc, type, plugin)); holograms.put(id, new FussballHologram(id, loc, type, plugin, loadCustomTextTemplates(path)));
} }
plugin.getLogger().info("[Hologram] " + holograms.size() + " Hologramme geladen."); plugin.getLogger().info("[Hologram] " + holograms.size() + " Hologramme geladen.");
@@ -123,10 +127,12 @@ public class HologramManager implements Listener {
/** /**
* Erstellt ein neues Hologramm und speichert es in holograms.yml. * Erstellt ein neues Hologramm und speichert es in holograms.yml.
* Falls die ID bereits existiert, wird das alte sauber entfernt. * Falls die ID bereits existiert, wird kein Hologramm ersetzt.
*/ */
public boolean createHologram(String id, Location loc, FussballHologram.HoloType type) { public boolean createHologram(String id, Location loc, FussballHologram.HoloType type) {
if (holograms.containsKey(id)) removeHologram(id); if (holograms.containsKey(id)) {
return false;
}
String path = "holograms." + id; String path = "holograms." + id;
holoConfig.set(path + ".world", loc.getWorld().getName()); holoConfig.set(path + ".world", loc.getWorld().getName());
@@ -187,6 +193,7 @@ public class HologramManager implements Listener {
/** Alle Hologramme neu laden (z.B. nach /fb hologram reload) */ /** Alle Hologramme neu laden (z.B. nach /fb hologram reload) */
public void reload() { public void reload() {
plugin.reloadConfig();
loadConfig(); loadConfig();
loadHolograms(); loadHolograms();
// Für alle Online-Spieler sofort rendern // Für alle Online-Spieler sofort rendern
@@ -195,6 +202,51 @@ public class HologramManager implements Listener {
} }
} }
public FussballHologram getHologram(String id) {
return holograms.get(id);
}
public boolean setCustomText(String id, FussballHologram.HoloType type, List<String> lines) {
FussballHologram holo = holograms.get(id);
if (holo == null) {
return false;
}
List<String> sanitized = new ArrayList<>(lines);
holo.setCustomText(type, sanitized);
holoConfig.set(getTextPath(id, type), sanitized);
saveConfig();
rerenderHologram(holo);
return true;
}
public boolean resetCustomText(String id, FussballHologram.HoloType type) {
FussballHologram holo = holograms.get(id);
if (holo == null) {
return false;
}
holo.clearCustomText(type);
holoConfig.set(getTextPath(id, type), null);
saveConfig();
rerenderHologram(holo);
return true;
}
public List<String> getCustomText(String id, FussballHologram.HoloType type) {
FussballHologram holo = holograms.get(id);
if (holo == null) {
return Collections.emptyList();
}
return holo.getCustomText(type);
}
public void refreshAll() {
for (Player player : Bukkit.getOnlinePlayers()) {
holograms.values().forEach(h -> h.renderForPlayer(player));
}
}
// ── Render-Task ────────────────────────────────────────────────────────── // ── Render-Task ──────────────────────────────────────────────────────────
/** /**
@@ -294,4 +346,25 @@ public class HologramManager implements Listener {
/** @return Set aller Hologramm-IDs (für Tab-Completion) */ /** @return Set aller Hologramm-IDs (für Tab-Completion) */
public Set<String> getHologramIds() { return holograms.keySet(); } public Set<String> getHologramIds() { return holograms.keySet(); }
private Map<FussballHologram.HoloType, List<String>> loadCustomTextTemplates(String path) {
Map<FussballHologram.HoloType, List<String>> templates = new EnumMap<>(FussballHologram.HoloType.class);
for (FussballHologram.HoloType holoType : FussballHologram.HoloType.values()) {
List<String> lines = holoConfig.getStringList(path + ".text." + holoType.name().toLowerCase());
if (!lines.isEmpty()) {
templates.put(holoType, lines);
}
}
return templates;
}
private String getTextPath(String id, FussballHologram.HoloType type) {
return "holograms." + id + ".text." + type.name().toLowerCase();
}
private void rerenderHologram(FussballHologram holo) {
for (Player player : Bukkit.getOnlinePlayers()) {
holo.renderForPlayer(player);
}
}
} }

View File

@@ -124,6 +124,34 @@ public class StatsManager {
save(); save();
} }
public boolean resetStats(UUID uuid) {
boolean removed = cache.remove(uuid) != null;
if (!removed) {
return false;
}
save();
return true;
}
public int resetAllStats() {
int count = cache.size();
if (count == 0) {
return 0;
}
cache.clear();
save();
return count;
}
public UUID findPlayerUuidByName(String playerName) {
for (Map.Entry<UUID, PlayerStats> entry : cache.entrySet()) {
if (entry.getValue().name != null && entry.getValue().name.equalsIgnoreCase(playerName)) {
return entry.getKey();
}
}
return null;
}
/** Gibt die Top-N-Torschützen zurück, sortiert nach Toren */ /** Gibt die Top-N-Torschützen zurück, sortiert nach Toren */
public List<Map.Entry<UUID, PlayerStats>> getTopScorers(int limit) { public List<Map.Entry<UUID, PlayerStats>> getTopScorers(int limit) {
List<Map.Entry<UUID, PlayerStats>> list = new ArrayList<>(cache.entrySet()); List<Map.Entry<UUID, PlayerStats>> list = new ArrayList<>(cache.entrySet());

View File

@@ -17,6 +17,29 @@ import java.util.function.Consumer;
* new UpdateChecker(this, RESOURCE_ID).getVersion(version -> { ... }); * new UpdateChecker(this, RESOURCE_ID).getVersion(version -> { ... });
*/ */
public class UpdateChecker { public class UpdateChecker {
/**
* Vergleicht zwei Versionsnummern (z.B. "1.0.3" und "1.0.2").
* Gibt >0 zurück, wenn v1 > v2, <0 wenn v1 < v2, 0 wenn gleich.
*/
public static int compareVersions(String v1, String v2) {
String[] parts1 = v1.replace("v", "").split("\\.");
String[] parts2 = v2.replace("v", "").split("\\.");
int len = Math.max(parts1.length, parts2.length);
for (int i = 0; i < len; i++) {
int n1 = i < parts1.length ? parseIntSafe(parts1[i]) : 0;
int n2 = i < parts2.length ? parseIntSafe(parts2[i]) : 0;
if (n1 != n2) return Integer.compare(n1, n2);
}
return 0;
}
private static int parseIntSafe(String s) {
try {
return Integer.parseInt(s.replaceAll("[^0-9]", ""));
} catch (NumberFormatException e) {
return 0;
}
}
private final JavaPlugin plugin; private final JavaPlugin plugin;
private final int resourceId; private final int resourceId;

View File

@@ -61,6 +61,24 @@ atmosphere:
enabled: true enabled: true
goal-fireworks: 5 # Anzahl Feuerwerke bei einem Tor (0 = deaktiviert) goal-fireworks: 5 # Anzahl Feuerwerke bei einem Tor (0 = deaktiviert)
# ── Hologramm-Farben ────────────────────────────────────────────────────────
# Verwende &-Codes (z.B. &6 = Gold, &c = Rot, &9 = Blau, &l = Fett, &o = Kursiv)
# Änderungen werden nach /fb hologram reload wirksam.
holograms:
goals-title-color: "&6&l" # Titel Tore-Hologramm (Standard: Gold + Fett)
goals-value-color: "&4" # Tor-Anzahl in der Liste (Standard: Dunkelrot)
wins-title-color: "&2&l" # Titel Siege-Hologramm (Standard: Dunkelgrün + Fett)
wins-value-color: "&2" # Siege-Anzahl in der Liste (Standard: Dunkelgrün)
name-color: "&0" # Spielername in der Liste (Standard: Schwarz)
label-color: "&8" # Beschriftungen (Tore/Siege) (Standard: Dunkelgrau)
separator-color: "&8&m" # Trennlinie (Standard: Durchgestrichen)
toggle-color: "&8&o" # Umschalte-Hinweis (Standard: Kursiv Dunkelgrau)
match-header-color: "&e&l" # Match-Header (Standard: Gelb + Fett)
match-score-red: "&c&l" # Rot-Team Spielstand (Standard: Rot + Fett)
match-score-blue: "&9&l" # Blau-Team Spielstand (Standard: Blau + Fett)
match-time-color: "&e" # Spielzeit (Standard: Gelb)
match-injury-color: "&c" # Nachspielzeit (Standard: Rot)
# ── Nachrichten (alle editierbar) ───────────────────────────────────────────── # ── Nachrichten (alle editierbar) ─────────────────────────────────────────────
# Verfügbare Platzhalter je nach Kontext: # Verfügbare Platzhalter je nach Kontext:
# {player} = Spielername # {player} = Spielername

View File

@@ -1,5 +1,5 @@
name: Fussball name: Fussball
version: 1.0.1 version: 1.0.3
main: de.fussball.plugin.Fussball main: de.fussball.plugin.Fussball
api-version: 1.21 api-version: 1.21
author: M_Viper author: M_Viper
@@ -19,5 +19,5 @@ permissions:
commands: commands:
fussball: fussball:
description: Hauptbefehl des Fußball-Plugins description: Hauptbefehl des Fußball-Plugins
usage: /fussball <join|leave|spectate|team|list|stats|top|history|create|delete|setup|stop|setgk|dropball|debug|hologram> usage: "/fussball <join|leave|spectate|team|list|stats|top|history|create|delete|setup|stop|setgk|dropball|debug|hologram> | Admin: stats reset, hologram text/textpreview/textreset"
aliases: [fb, soccer] aliases: [fb, soccer]